@impulselab/cli 0.1.4 → 0.1.6
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 +181 -87
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -246,9 +246,9 @@ var ModuleManifestSchema = z5.object({
|
|
|
246
246
|
/** Names of sub-modules available under this parent module (e.g. ["quote-to-cash", "gocardless"]). */
|
|
247
247
|
subModules: z5.array(z5.string()).default([]),
|
|
248
248
|
dependencies: z5.array(ModuleDependencySchema).default([]),
|
|
249
|
-
moduleDependencies: z5.array(
|
|
249
|
+
moduleDependencies: z5.array(ModuleDependencySchema).default([]),
|
|
250
250
|
/** Optional module dependencies that enhance this module but are not required. */
|
|
251
|
-
optionalModuleDependencies: z5.array(
|
|
251
|
+
optionalModuleDependencies: z5.array(ModuleDependencySchema).default([]),
|
|
252
252
|
files: z5.array(ModuleFileSchema).default([]),
|
|
253
253
|
transforms: z5.array(ModuleTransformSchema).default([]),
|
|
254
254
|
/** Documentation metadata listing env vars this module requires (displayed in install summary). */
|
|
@@ -264,16 +264,22 @@ var ModuleManifestSchema = z5.object({
|
|
|
264
264
|
targetPackage: z5.enum(["database", "server", "web", "root"]).default("root")
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
// src/registry/constants.ts
|
|
268
|
+
var registryConstants = {
|
|
269
|
+
githubOrg: "impulse-studio",
|
|
270
|
+
githubRepo: "impulse-modules",
|
|
271
|
+
githubBranch: "main",
|
|
272
|
+
modulesDir: "modules",
|
|
273
|
+
subModulesDirName: "sub-modules",
|
|
274
|
+
manifestFileName: "module.json"
|
|
275
|
+
};
|
|
276
|
+
|
|
267
277
|
// src/registry/github-urls.ts
|
|
268
278
|
import { execSync } from "child_process";
|
|
269
|
-
var GITHUB_ORG = "impulse-studio";
|
|
270
|
-
var GITHUB_REPO = "impulse-modules";
|
|
271
|
-
var GITHUB_BRANCH = "main";
|
|
272
|
-
var MODULES_DIR = "modules";
|
|
273
279
|
var githubUrls = {
|
|
274
|
-
rawFile: (registryPath, file) => `https://raw.githubusercontent.com/${
|
|
275
|
-
moduleList: () => `https://api.github.com/repos/${
|
|
276
|
-
subModulesList: (parentModule) => `https://api.github.com/repos/${
|
|
280
|
+
rawFile: (registryPath, file) => `https://raw.githubusercontent.com/${registryConstants.githubOrg}/${registryConstants.githubRepo}/${registryConstants.githubBranch}/${registryConstants.modulesDir}/${registryPath}/${file}`,
|
|
281
|
+
moduleList: () => `https://api.github.com/repos/${registryConstants.githubOrg}/${registryConstants.githubRepo}/contents/${registryConstants.modulesDir}`,
|
|
282
|
+
subModulesList: (parentModule) => `https://api.github.com/repos/${registryConstants.githubOrg}/${registryConstants.githubRepo}/contents/${registryConstants.modulesDir}/${parentModule}/${registryConstants.subModulesDirName}`
|
|
277
283
|
};
|
|
278
284
|
var _cachedToken;
|
|
279
285
|
function getGitHubToken() {
|
|
@@ -315,7 +321,7 @@ function parseModuleId(moduleId) {
|
|
|
315
321
|
function moduleRegistryPath(moduleId) {
|
|
316
322
|
const { parent, child } = parseModuleId(moduleId);
|
|
317
323
|
if (child === null) return parent;
|
|
318
|
-
return `${parent}
|
|
324
|
+
return `${parent}/${registryConstants.subModulesDirName}/${child}`;
|
|
319
325
|
}
|
|
320
326
|
|
|
321
327
|
// src/registry/fetch-module-manifest.ts
|
|
@@ -323,7 +329,7 @@ var { readJson: readJson3, pathExists: pathExists4 } = fsExtra5;
|
|
|
323
329
|
async function fetchModuleManifest(moduleId, localPath) {
|
|
324
330
|
const registryPath = moduleRegistryPath(moduleId);
|
|
325
331
|
if (localPath) {
|
|
326
|
-
const file = path3.join(localPath, registryPath,
|
|
332
|
+
const file = path3.join(localPath, registryPath, registryConstants.manifestFileName);
|
|
327
333
|
if (!await pathExists4(file)) {
|
|
328
334
|
throw new Error(`Local module not found: ${file}`);
|
|
329
335
|
}
|
|
@@ -334,7 +340,7 @@ async function fetchModuleManifest(moduleId, localPath) {
|
|
|
334
340
|
}
|
|
335
341
|
return parsed2.data;
|
|
336
342
|
}
|
|
337
|
-
const url = githubUrls.rawFile(registryPath,
|
|
343
|
+
const url = githubUrls.rawFile(registryPath, registryConstants.manifestFileName);
|
|
338
344
|
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
339
345
|
if (!res.ok) {
|
|
340
346
|
if (res.status === 404) {
|
|
@@ -362,14 +368,22 @@ async function listAvailableModules(localPath) {
|
|
|
362
368
|
const result = [];
|
|
363
369
|
for (const entry of entries2) {
|
|
364
370
|
if (!entry.isDirectory()) continue;
|
|
365
|
-
const manifestFile = path4.join(
|
|
371
|
+
const manifestFile = path4.join(
|
|
372
|
+
localPath,
|
|
373
|
+
entry.name,
|
|
374
|
+
registryConstants.manifestFileName
|
|
375
|
+
);
|
|
366
376
|
if (!await pathExists5(manifestFile)) continue;
|
|
367
377
|
try {
|
|
368
378
|
const raw = await readJson4(manifestFile);
|
|
369
379
|
const parsed = ModuleManifestSchema.safeParse(raw);
|
|
370
380
|
if (!parsed.success) continue;
|
|
371
381
|
let subModules;
|
|
372
|
-
const subModulesDir = path4.join(
|
|
382
|
+
const subModulesDir = path4.join(
|
|
383
|
+
localPath,
|
|
384
|
+
entry.name,
|
|
385
|
+
registryConstants.subModulesDirName
|
|
386
|
+
);
|
|
373
387
|
if (await pathExists5(subModulesDir)) {
|
|
374
388
|
const subEntries = await readdir(subModulesDir, { withFileTypes: true });
|
|
375
389
|
subModules = subEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
@@ -428,10 +442,10 @@ async function listAvailableModules(localPath) {
|
|
|
428
442
|
// src/registry/fetch-module-file.ts
|
|
429
443
|
async function fetchModuleFile(moduleName, fileSrc, localPath) {
|
|
430
444
|
if (localPath) {
|
|
431
|
-
const { readFile:
|
|
445
|
+
const { readFile: readFile7 } = await import("fs/promises");
|
|
432
446
|
const { join } = await import("path");
|
|
433
447
|
const file = join(localPath, moduleName, fileSrc);
|
|
434
|
-
return
|
|
448
|
+
return readFile7(file, "utf-8");
|
|
435
449
|
}
|
|
436
450
|
const url = githubUrls.rawFile(moduleName, fileSrc);
|
|
437
451
|
const res = await fetch(url, { headers: getGitHubHeaders() });
|
|
@@ -443,6 +457,7 @@ async function fetchModuleFile(moduleName, fileSrc, localPath) {
|
|
|
443
457
|
|
|
444
458
|
// src/installer.ts
|
|
445
459
|
import fsExtra7 from "fs-extra";
|
|
460
|
+
import { readFile } from "fs/promises";
|
|
446
461
|
import path5 from "path";
|
|
447
462
|
import * as p2 from "@clack/prompts";
|
|
448
463
|
var { outputFile, pathExists: pathExists6 } = fsExtra7;
|
|
@@ -461,18 +476,16 @@ async function installFiles(options) {
|
|
|
461
476
|
const destAbs = path5.join(cwd, file.dest);
|
|
462
477
|
const exists = await pathExists6(destAbs);
|
|
463
478
|
if (exists) {
|
|
479
|
+
if (dryRun) {
|
|
480
|
+
results.push({ dest: file.dest, action: "would-overwrite" });
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
464
483
|
const content = await fetchModuleFile(moduleName, file.src, localPath);
|
|
465
|
-
const existing = await
|
|
466
|
-
(fs) => fs.readFile(destAbs, "utf-8")
|
|
467
|
-
);
|
|
484
|
+
const existing = await readFile(destAbs, "utf-8");
|
|
468
485
|
if (existing === content) {
|
|
469
486
|
results.push({ dest: file.dest, action: "skipped" });
|
|
470
487
|
continue;
|
|
471
488
|
}
|
|
472
|
-
if (dryRun) {
|
|
473
|
-
results.push({ dest: file.dest, action: "would-overwrite" });
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
489
|
const answer = await p2.confirm({
|
|
477
490
|
message: `File already exists: ${file.dest} \u2014 overwrite?`,
|
|
478
491
|
initialValue: false
|
|
@@ -499,7 +512,7 @@ async function installFiles(options) {
|
|
|
499
512
|
// src/transforms/append-export.ts
|
|
500
513
|
import fsExtra8 from "fs-extra";
|
|
501
514
|
import path6 from "path";
|
|
502
|
-
var { readFile, outputFile: outputFile2, pathExists: pathExists7 } = fsExtra8;
|
|
515
|
+
var { readFile: readFile2, outputFile: outputFile2, pathExists: pathExists7 } = fsExtra8;
|
|
503
516
|
async function appendExport(target, value, cwd, dryRun) {
|
|
504
517
|
const file = path6.join(cwd, target);
|
|
505
518
|
if (!await pathExists7(file)) {
|
|
@@ -509,7 +522,7 @@ async function appendExport(target, value, cwd, dryRun) {
|
|
|
509
522
|
}
|
|
510
523
|
return;
|
|
511
524
|
}
|
|
512
|
-
const content = await
|
|
525
|
+
const content = await readFile2(file, "utf-8");
|
|
513
526
|
if (content.includes(value)) return;
|
|
514
527
|
if (!dryRun) {
|
|
515
528
|
const newContent = content.endsWith("\n") ? `${content}${value}
|
|
@@ -523,7 +536,7 @@ ${value}
|
|
|
523
536
|
// src/transforms/register-route.ts
|
|
524
537
|
import fsExtra9 from "fs-extra";
|
|
525
538
|
import path7 from "path";
|
|
526
|
-
var { readFile:
|
|
539
|
+
var { readFile: readFile3, outputFile: outputFile3, pathExists: pathExists8 } = fsExtra9;
|
|
527
540
|
async function registerRoute(target, value, cwd, dryRun) {
|
|
528
541
|
const file = path7.join(cwd, target);
|
|
529
542
|
if (!await pathExists8(file)) {
|
|
@@ -531,7 +544,7 @@ async function registerRoute(target, value, cwd, dryRun) {
|
|
|
531
544
|
`register-route: target file not found: ${target}. Please ensure your router file exists.`
|
|
532
545
|
);
|
|
533
546
|
}
|
|
534
|
-
const content = await
|
|
547
|
+
const content = await readFile3(file, "utf-8");
|
|
535
548
|
if (content.includes(value)) return;
|
|
536
549
|
const routerPattern = /^([ \t]*\})[ \t]*(?:satisfies|as)\s/m;
|
|
537
550
|
const match = routerPattern.exec(content);
|
|
@@ -551,7 +564,7 @@ async function registerRoute(target, value, cwd, dryRun) {
|
|
|
551
564
|
// src/transforms/add-nav-item.ts
|
|
552
565
|
import fsExtra10 from "fs-extra";
|
|
553
566
|
import path8 from "path";
|
|
554
|
-
var { readFile:
|
|
567
|
+
var { readFile: readFile4, outputFile: outputFile4, pathExists: pathExists9 } = fsExtra10;
|
|
555
568
|
async function addNavItem(target, value, cwd, dryRun) {
|
|
556
569
|
const file = path8.join(cwd, target);
|
|
557
570
|
if (!await pathExists9(file)) {
|
|
@@ -559,7 +572,7 @@ async function addNavItem(target, value, cwd, dryRun) {
|
|
|
559
572
|
`add-nav-item: target file not found: ${target}`
|
|
560
573
|
);
|
|
561
574
|
}
|
|
562
|
-
const content = await
|
|
575
|
+
const content = await readFile4(file, "utf-8");
|
|
563
576
|
if (content.includes(value)) return;
|
|
564
577
|
const openPattern = /(?:const\s+\w+(?:\s*:\s*[\w<>\[\], ]+)?\s*=\s*\[|items\s*:\s*\[)/;
|
|
565
578
|
const openMatch = openPattern.exec(content);
|
|
@@ -596,7 +609,7 @@ async function addNavItem(target, value, cwd, dryRun) {
|
|
|
596
609
|
// src/transforms/merge-schema.ts
|
|
597
610
|
import fsExtra11 from "fs-extra";
|
|
598
611
|
import path9 from "path";
|
|
599
|
-
var { readFile:
|
|
612
|
+
var { readFile: readFile5, outputFile: outputFile5, pathExists: pathExists10 } = fsExtra11;
|
|
600
613
|
async function mergeSchema(target, value, cwd, dryRun) {
|
|
601
614
|
const file = path9.join(cwd, target);
|
|
602
615
|
if (!await pathExists10(file)) {
|
|
@@ -606,7 +619,7 @@ async function mergeSchema(target, value, cwd, dryRun) {
|
|
|
606
619
|
}
|
|
607
620
|
return;
|
|
608
621
|
}
|
|
609
|
-
const content = await
|
|
622
|
+
const content = await readFile5(file, "utf-8");
|
|
610
623
|
if (content.includes(value)) return;
|
|
611
624
|
if (!dryRun) {
|
|
612
625
|
const newContent = content.endsWith("\n") ? `${content}${value}
|
|
@@ -621,12 +634,12 @@ ${value}
|
|
|
621
634
|
import fsExtra12 from "fs-extra";
|
|
622
635
|
import path10 from "path";
|
|
623
636
|
import * as p3 from "@clack/prompts";
|
|
624
|
-
var { readFile:
|
|
637
|
+
var { readFile: readFile6, outputFile: outputFile6, pathExists: pathExists11 } = fsExtra12;
|
|
625
638
|
async function addEnv(target, value, cwd, dryRun) {
|
|
626
639
|
const file = path10.join(cwd, target);
|
|
627
640
|
let content = "";
|
|
628
641
|
if (await pathExists11(file)) {
|
|
629
|
-
content = await
|
|
642
|
+
content = await readFile6(file, "utf-8");
|
|
630
643
|
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
631
644
|
const keyPattern = new RegExp(`^${escaped}=`, "m");
|
|
632
645
|
if (keyPattern.test(content)) return;
|
|
@@ -692,9 +705,8 @@ function authPath() {
|
|
|
692
705
|
import { z as z6 } from "zod";
|
|
693
706
|
var AuthCredentialsSchema = z6.object({
|
|
694
707
|
token: z6.string(),
|
|
695
|
-
refreshToken: z6.string().optional(),
|
|
696
708
|
expiresAt: z6.string().optional(),
|
|
697
|
-
email: z6.string()
|
|
709
|
+
email: z6.string().optional()
|
|
698
710
|
});
|
|
699
711
|
|
|
700
712
|
// src/auth/read-auth.ts
|
|
@@ -724,21 +736,28 @@ async function requireAuth() {
|
|
|
724
736
|
}
|
|
725
737
|
|
|
726
738
|
// src/commands/add.ts
|
|
727
|
-
async function
|
|
739
|
+
async function getManifest(moduleId, localPath, manifests) {
|
|
740
|
+
const cached = manifests.get(moduleId);
|
|
741
|
+
if (cached) return cached;
|
|
742
|
+
const manifest = await fetchModuleManifest(moduleId, localPath);
|
|
743
|
+
manifests.set(moduleId, manifest);
|
|
744
|
+
return manifest;
|
|
745
|
+
}
|
|
746
|
+
async function resolveModuleDeps(moduleId, localPath, manifests, resolved, orderedModules) {
|
|
728
747
|
if (resolved.has(moduleId)) return;
|
|
729
748
|
resolved.add(moduleId);
|
|
730
|
-
const manifest = await
|
|
749
|
+
const manifest = await getManifest(moduleId, localPath, manifests);
|
|
731
750
|
for (const dep of manifest.moduleDependencies) {
|
|
732
|
-
await resolveModuleDeps(dep, localPath, resolved, orderedModules);
|
|
751
|
+
await resolveModuleDeps(dep, localPath, manifests, resolved, orderedModules);
|
|
733
752
|
}
|
|
734
753
|
orderedModules.push(moduleId);
|
|
735
754
|
}
|
|
736
|
-
async function resolveWithParent(moduleId, localPath, installedNames, resolved, orderedModules) {
|
|
755
|
+
async function resolveWithParent(moduleId, localPath, installedNames, manifests, resolved, orderedModules) {
|
|
737
756
|
const { parent, child } = parseModuleId(moduleId);
|
|
738
757
|
if (child !== null && !installedNames.has(parent)) {
|
|
739
|
-
await resolveModuleDeps(parent, localPath, resolved, orderedModules);
|
|
758
|
+
await resolveModuleDeps(parent, localPath, manifests, resolved, orderedModules);
|
|
740
759
|
}
|
|
741
|
-
await resolveModuleDeps(moduleId, localPath, resolved, orderedModules);
|
|
760
|
+
await resolveModuleDeps(moduleId, localPath, manifests, resolved, orderedModules);
|
|
742
761
|
}
|
|
743
762
|
function detectPackageManager(cwd) {
|
|
744
763
|
if (existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
@@ -774,9 +793,7 @@ async function installModule(moduleId, manifest, cwd, dryRun, installedModules,
|
|
|
774
793
|
});
|
|
775
794
|
for (const f of installed) {
|
|
776
795
|
if (f.action === "conditional-skip") {
|
|
777
|
-
|
|
778
|
-
p5.log.message(` \u25CB ${f.dest} [skipped: ${f.reason}]`);
|
|
779
|
-
}
|
|
796
|
+
p5.log.message(` \u25CB ${f.dest} [skipped: ${f.reason}]`);
|
|
780
797
|
continue;
|
|
781
798
|
}
|
|
782
799
|
installedDests.push(f.dest);
|
|
@@ -853,7 +870,7 @@ async function pickModulesInteractively(localPath) {
|
|
|
853
870
|
return [...result];
|
|
854
871
|
}
|
|
855
872
|
async function runAdd(options) {
|
|
856
|
-
let { moduleNames, cwd, dryRun, localPath, withSubModules = [] } = options;
|
|
873
|
+
let { moduleNames, cwd, dryRun, localPath, withSubModules = [], allowScripts = false } = options;
|
|
857
874
|
let allTargets;
|
|
858
875
|
if (moduleNames.length === 0) {
|
|
859
876
|
p5.intro(`impulse add${dryRun ? " [dry-run]" : ""}`);
|
|
@@ -885,6 +902,7 @@ async function runAdd(options) {
|
|
|
885
902
|
process.exit(1);
|
|
886
903
|
}
|
|
887
904
|
const installedNames = new Set(config.installedModules.map((m) => m.name));
|
|
905
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
888
906
|
if (withSubModules.length === 0) {
|
|
889
907
|
const alreadyInstalled = allTargets.filter((id) => {
|
|
890
908
|
const { child } = parseModuleId(id);
|
|
@@ -906,7 +924,7 @@ async function runAdd(options) {
|
|
|
906
924
|
}
|
|
907
925
|
}
|
|
908
926
|
if (moduleNames.length === 1 && withSubModules.length > 0) {
|
|
909
|
-
const parentManifest = await
|
|
927
|
+
const parentManifest = await getManifest(moduleNames[0], localPath, manifests).catch(() => null);
|
|
910
928
|
if (parentManifest) {
|
|
911
929
|
if (parentManifest.subModules.length === 0) {
|
|
912
930
|
p5.cancel(`"${moduleNames[0]}" has no declared sub-modules.`);
|
|
@@ -928,7 +946,7 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
928
946
|
const orderedModules = [];
|
|
929
947
|
try {
|
|
930
948
|
for (const target of allTargets) {
|
|
931
|
-
await resolveWithParent(target, localPath, installedNames, resolved, orderedModules);
|
|
949
|
+
await resolveWithParent(target, localPath, installedNames, manifests, resolved, orderedModules);
|
|
932
950
|
}
|
|
933
951
|
} catch (err) {
|
|
934
952
|
s.stop("Dependency resolution failed.");
|
|
@@ -936,11 +954,14 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
936
954
|
process.exit(1);
|
|
937
955
|
}
|
|
938
956
|
s.stop(`Resolved: ${orderedModules.join(" \u2192 ")}`);
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
957
|
+
const orderedManifests = orderedModules.map((id) => {
|
|
958
|
+
const manifest = manifests.get(id);
|
|
959
|
+
if (!manifest) {
|
|
960
|
+
throw new Error(`Missing manifest for resolved module "${id}".`);
|
|
961
|
+
}
|
|
962
|
+
return [id, manifest];
|
|
963
|
+
});
|
|
964
|
+
for (const [id, manifest] of orderedManifests) {
|
|
944
965
|
if (installedNames.has(id)) continue;
|
|
945
966
|
for (const incompatible of manifest.incompatibleWith) {
|
|
946
967
|
if (installedNames.has(incompatible)) {
|
|
@@ -954,13 +975,13 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
954
975
|
}
|
|
955
976
|
}
|
|
956
977
|
const allDeps = /* @__PURE__ */ new Set();
|
|
957
|
-
for (const manifest of
|
|
978
|
+
for (const [, manifest] of orderedManifests) {
|
|
958
979
|
for (const dep of manifest.dependencies) {
|
|
959
980
|
allDeps.add(dep);
|
|
960
981
|
}
|
|
961
982
|
}
|
|
962
983
|
p5.log.message("\nSummary of changes:");
|
|
963
|
-
for (const [id, manifest] of
|
|
984
|
+
for (const [id, manifest] of orderedManifests) {
|
|
964
985
|
p5.log.message(`
|
|
965
986
|
Module: ${id}@${manifest.version}`);
|
|
966
987
|
for (const file of manifest.files) {
|
|
@@ -971,7 +992,7 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
971
992
|
}
|
|
972
993
|
}
|
|
973
994
|
const allEnvVars = /* @__PURE__ */ new Set();
|
|
974
|
-
for (const manifest of
|
|
995
|
+
for (const [, manifest] of orderedManifests) {
|
|
975
996
|
for (const envVar of manifest.envVars) {
|
|
976
997
|
allEnvVars.add(envVar);
|
|
977
998
|
}
|
|
@@ -1020,23 +1041,64 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
1020
1041
|
installedNames.add(targetId);
|
|
1021
1042
|
}
|
|
1022
1043
|
installNpmDeps([...allDeps], cwd, dryRun);
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1044
|
+
const allPostInstallHooks = [];
|
|
1045
|
+
for (const { name, hooks } of depPostInstallHooks) {
|
|
1046
|
+
if (hooks.length > 0) allPostInstallHooks.push({ name, hooks });
|
|
1047
|
+
}
|
|
1048
|
+
for (const targetId of targetModules) {
|
|
1049
|
+
const targetManifest = manifests.get(targetId);
|
|
1050
|
+
if (targetManifest?.postInstall?.length) {
|
|
1051
|
+
allPostInstallHooks.push({ name: targetId, hooks: targetManifest.postInstall });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (allPostInstallHooks.length > 0) {
|
|
1055
|
+
const allHookLines = [];
|
|
1056
|
+
for (const { name, hooks } of allPostInstallHooks) {
|
|
1026
1057
|
for (const hook of hooks) {
|
|
1027
|
-
|
|
1028
|
-
execSync2(hook, { cwd, stdio: "inherit" });
|
|
1058
|
+
allHookLines.push(` [${name}] $ ${hook}`);
|
|
1029
1059
|
}
|
|
1030
1060
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1061
|
+
if (dryRun) {
|
|
1062
|
+
p5.log.message("\nPost-install hooks that would run:");
|
|
1063
|
+
for (const line of allHookLines) {
|
|
1064
|
+
p5.log.message(line);
|
|
1065
|
+
}
|
|
1066
|
+
} else if (!allowScripts && process.stdin.isTTY === false) {
|
|
1067
|
+
p5.log.warn(
|
|
1068
|
+
`Skipping ${allHookLines.length} post-install hook(s) \u2014 non-interactive mode detected. Pass --allow-scripts to run them.`
|
|
1069
|
+
);
|
|
1070
|
+
for (const line of allHookLines) {
|
|
1071
|
+
p5.log.message(line);
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
p5.log.message("\nPost-install hooks to run:");
|
|
1075
|
+
for (const line of allHookLines) {
|
|
1076
|
+
p5.log.message(line);
|
|
1077
|
+
}
|
|
1078
|
+
let runHooks;
|
|
1079
|
+
if (allowScripts) {
|
|
1080
|
+
runHooks = true;
|
|
1081
|
+
} else {
|
|
1082
|
+
const confirmed = await p5.confirm({
|
|
1083
|
+
message: "Run post-install hooks?",
|
|
1084
|
+
initialValue: true
|
|
1085
|
+
});
|
|
1086
|
+
if (p5.isCancel(confirmed)) {
|
|
1087
|
+
p5.outro("Cancelled.");
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
runHooks = confirmed;
|
|
1091
|
+
}
|
|
1092
|
+
if (runHooks) {
|
|
1093
|
+
for (const { name, hooks } of allPostInstallHooks) {
|
|
1094
|
+
p5.log.step(`Running post-install hooks for ${name}...`);
|
|
1095
|
+
for (const hook of hooks) {
|
|
1096
|
+
p5.log.message(` $ ${hook}`);
|
|
1097
|
+
execSync2(hook, { cwd, stdio: "inherit" });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
} else {
|
|
1101
|
+
p5.log.warn("Post-install hooks skipped.");
|
|
1040
1102
|
}
|
|
1041
1103
|
}
|
|
1042
1104
|
}
|
|
@@ -1149,19 +1211,20 @@ import * as p7 from "@clack/prompts";
|
|
|
1149
1211
|
|
|
1150
1212
|
// src/auth/device-flow.ts
|
|
1151
1213
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1152
|
-
var IMPULSE_BASE_URL = process.env.IMPULSE_BASE_URL ?? "https://
|
|
1214
|
+
var IMPULSE_BASE_URL = process.env.IMPULSE_BASE_URL ?? "https://impulselab.ai";
|
|
1215
|
+
var CLIENT_ID = "impulse-cli";
|
|
1153
1216
|
async function requestDeviceCode() {
|
|
1154
1217
|
const res = await fetch(`${IMPULSE_BASE_URL}/api/auth/device/code`, {
|
|
1155
1218
|
method: "POST",
|
|
1156
1219
|
headers: { "Content-Type": "application/json" },
|
|
1157
|
-
body: JSON.stringify({
|
|
1220
|
+
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
1158
1221
|
});
|
|
1159
1222
|
if (!res.ok) {
|
|
1160
1223
|
throw new Error(`Failed to initiate device flow: ${res.status} ${res.statusText}`);
|
|
1161
1224
|
}
|
|
1162
1225
|
const data = await res.json();
|
|
1163
1226
|
assertDeviceCodeResponse(data);
|
|
1164
|
-
return data;
|
|
1227
|
+
return mapDeviceCodeResponse(data);
|
|
1165
1228
|
}
|
|
1166
1229
|
async function pollDeviceToken(deviceCode) {
|
|
1167
1230
|
let res;
|
|
@@ -1169,7 +1232,11 @@ async function pollDeviceToken(deviceCode) {
|
|
|
1169
1232
|
res = await fetch(`${IMPULSE_BASE_URL}/api/auth/device/token`, {
|
|
1170
1233
|
method: "POST",
|
|
1171
1234
|
headers: { "Content-Type": "application/json" },
|
|
1172
|
-
body: JSON.stringify({
|
|
1235
|
+
body: JSON.stringify({
|
|
1236
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1237
|
+
device_code: deviceCode,
|
|
1238
|
+
client_id: CLIENT_ID
|
|
1239
|
+
})
|
|
1173
1240
|
});
|
|
1174
1241
|
} catch {
|
|
1175
1242
|
return { status: "error", message: "Network error while polling for token" };
|
|
@@ -1177,10 +1244,7 @@ async function pollDeviceToken(deviceCode) {
|
|
|
1177
1244
|
if (res.status === 200) {
|
|
1178
1245
|
const data = await res.json();
|
|
1179
1246
|
assertDeviceTokenResponse(data);
|
|
1180
|
-
return { status: "authorized", data };
|
|
1181
|
-
}
|
|
1182
|
-
if (res.status === 202) {
|
|
1183
|
-
return { status: "pending" };
|
|
1247
|
+
return { status: "authorized", data: mapDeviceTokenResponse(data) };
|
|
1184
1248
|
}
|
|
1185
1249
|
if (res.status === 400) {
|
|
1186
1250
|
let body;
|
|
@@ -1190,8 +1254,10 @@ async function pollDeviceToken(deviceCode) {
|
|
|
1190
1254
|
body = {};
|
|
1191
1255
|
}
|
|
1192
1256
|
const error = typeof body === "object" && body !== null && "error" in body ? String(body.error) : "";
|
|
1257
|
+
if (error === "authorization_pending") return { status: "pending" };
|
|
1193
1258
|
if (error === "slow_down") return { status: "slow_down" };
|
|
1194
|
-
if (error === "
|
|
1259
|
+
if (error === "expired_token") return { status: "expired" };
|
|
1260
|
+
if (error === "access_denied") return { status: "denied" };
|
|
1195
1261
|
return { status: "error", message: error || "Device code error" };
|
|
1196
1262
|
}
|
|
1197
1263
|
return { status: "error", message: `Unexpected response: ${res.status}` };
|
|
@@ -1210,15 +1276,32 @@ function openBrowser(url) {
|
|
|
1210
1276
|
}
|
|
1211
1277
|
}
|
|
1212
1278
|
function assertDeviceCodeResponse(data) {
|
|
1213
|
-
if (typeof data !== "object" || data === null || typeof data.
|
|
1279
|
+
if (typeof data !== "object" || data === null || typeof data.device_code !== "string" || typeof data.user_code !== "string" || typeof data.verification_uri !== "string") {
|
|
1214
1280
|
throw new Error("Invalid device code response from server");
|
|
1215
1281
|
}
|
|
1216
1282
|
}
|
|
1217
1283
|
function assertDeviceTokenResponse(data) {
|
|
1218
|
-
if (typeof data !== "object" || data === null || typeof data.
|
|
1284
|
+
if (typeof data !== "object" || data === null || typeof data.access_token !== "string") {
|
|
1219
1285
|
throw new Error("Invalid token response from server");
|
|
1220
1286
|
}
|
|
1221
1287
|
}
|
|
1288
|
+
function mapDeviceCodeResponse(raw) {
|
|
1289
|
+
return {
|
|
1290
|
+
deviceCode: raw.device_code,
|
|
1291
|
+
userCode: raw.user_code,
|
|
1292
|
+
verificationUri: raw.verification_uri,
|
|
1293
|
+
...raw.verification_uri_complete !== void 0 ? { verificationUriComplete: raw.verification_uri_complete } : {},
|
|
1294
|
+
expiresIn: raw.expires_in,
|
|
1295
|
+
interval: raw.interval
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
function mapDeviceTokenResponse(raw) {
|
|
1299
|
+
return {
|
|
1300
|
+
token: raw.access_token,
|
|
1301
|
+
...raw.expires_in !== void 0 ? { expiresIn: raw.expires_in } : {},
|
|
1302
|
+
...raw.email !== void 0 ? { email: raw.email } : {}
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1222
1305
|
|
|
1223
1306
|
// src/auth/write-auth.ts
|
|
1224
1307
|
import fsExtra14 from "fs-extra";
|
|
@@ -1238,7 +1321,7 @@ async function runLogin() {
|
|
|
1238
1321
|
const existing = await readAuth();
|
|
1239
1322
|
if (existing) {
|
|
1240
1323
|
const reauth = await p7.confirm({
|
|
1241
|
-
message: `Already logged in as ${existing.email}. Log in again?`,
|
|
1324
|
+
message: `Already logged in as ${existing.email ?? "current session"}. Log in again?`,
|
|
1242
1325
|
initialValue: false
|
|
1243
1326
|
});
|
|
1244
1327
|
if (p7.isCancel(reauth) || !reauth) {
|
|
@@ -1281,13 +1364,14 @@ Base URL: ${IMPULSE_BASE_URL}`);
|
|
|
1281
1364
|
const result = await pollDeviceToken(deviceCode.deviceCode);
|
|
1282
1365
|
if (result.status === "authorized") {
|
|
1283
1366
|
pollSpinner.stop("Authentication successful!");
|
|
1367
|
+
const expiresAt2 = result.data.expiresIn !== void 0 ? new Date(Date.now() + result.data.expiresIn * 1e3).toISOString() : void 0;
|
|
1284
1368
|
await writeAuth({
|
|
1285
1369
|
token: result.data.token,
|
|
1286
|
-
|
|
1287
|
-
expiresAt: result.data.expiresAt,
|
|
1370
|
+
expiresAt: expiresAt2,
|
|
1288
1371
|
email: result.data.email
|
|
1289
1372
|
});
|
|
1290
|
-
|
|
1373
|
+
const identity = result.data.email ?? "your account";
|
|
1374
|
+
p7.outro(`Logged in as ${identity}`);
|
|
1291
1375
|
return;
|
|
1292
1376
|
}
|
|
1293
1377
|
if (result.status === "slow_down") {
|
|
@@ -1299,6 +1383,11 @@ Base URL: ${IMPULSE_BASE_URL}`);
|
|
|
1299
1383
|
p7.cancel("Authentication timed out. Run `impulse login` again.");
|
|
1300
1384
|
process.exit(1);
|
|
1301
1385
|
}
|
|
1386
|
+
if (result.status === "denied") {
|
|
1387
|
+
pollSpinner.stop("Access denied.");
|
|
1388
|
+
p7.cancel("You denied the authorization request.");
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1302
1391
|
if (result.status === "error") {
|
|
1303
1392
|
pollSpinner.stop("Authentication failed.");
|
|
1304
1393
|
p7.cancel(result.message);
|
|
@@ -1335,7 +1424,7 @@ async function runLogout() {
|
|
|
1335
1424
|
return;
|
|
1336
1425
|
}
|
|
1337
1426
|
const confirm6 = await p8.confirm({
|
|
1338
|
-
message: `Log out ${credentials.email}?`,
|
|
1427
|
+
message: `Log out ${credentials.email ?? "current session"}?`,
|
|
1339
1428
|
initialValue: true
|
|
1340
1429
|
});
|
|
1341
1430
|
if (p8.isCancel(confirm6) || !confirm6) {
|
|
@@ -1355,7 +1444,7 @@ async function runWhoami() {
|
|
|
1355
1444
|
p9.cancel("Not authenticated. Run `impulse login` first.");
|
|
1356
1445
|
process.exit(1);
|
|
1357
1446
|
}
|
|
1358
|
-
p9.log.message(`Logged in as: ${credentials.email}`);
|
|
1447
|
+
p9.log.message(`Logged in as: ${credentials.email ?? "(unknown)"}`);
|
|
1359
1448
|
if (credentials.expiresAt) {
|
|
1360
1449
|
const expires = new Date(credentials.expiresAt);
|
|
1361
1450
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1387,11 +1476,16 @@ program.command("add [modules...]").description(
|
|
|
1387
1476
|
).option(
|
|
1388
1477
|
"--with <submodules>",
|
|
1389
1478
|
"Comma-separated sub-modules to install alongside the parent (e.g. --with quote-to-cash,gocardless)"
|
|
1479
|
+
).option(
|
|
1480
|
+
"--allow-scripts",
|
|
1481
|
+
"Run post-install hooks without prompting for confirmation (required in non-interactive environments)",
|
|
1482
|
+
false
|
|
1390
1483
|
).action(async (modules, options) => {
|
|
1391
1484
|
const addOpts = {
|
|
1392
1485
|
moduleNames: modules ?? [],
|
|
1393
1486
|
cwd: process.cwd(),
|
|
1394
|
-
dryRun: options.dryRun
|
|
1487
|
+
dryRun: options.dryRun,
|
|
1488
|
+
allowScripts: options.allowScripts
|
|
1395
1489
|
};
|
|
1396
1490
|
if (options.local !== void 0) addOpts.localPath = options.local;
|
|
1397
1491
|
if (options.with !== void 0) {
|