@impulselab/cli 0.1.1 → 0.1.3
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 +432 -64
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -197,8 +197,8 @@ async function runInit(options) {
|
|
|
197
197
|
// src/commands/add.ts
|
|
198
198
|
import { execSync as execSync2, execFileSync } from "child_process";
|
|
199
199
|
import { existsSync } from "fs";
|
|
200
|
-
import
|
|
201
|
-
import * as
|
|
200
|
+
import path12 from "path";
|
|
201
|
+
import * as p5 from "@clack/prompts";
|
|
202
202
|
|
|
203
203
|
// src/registry/fetch-module-manifest.ts
|
|
204
204
|
import fsExtra5 from "fs-extra";
|
|
@@ -652,6 +652,54 @@ async function runTransform(transform, cwd, dryRun) {
|
|
|
652
652
|
}
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
+
// src/auth/require-auth.ts
|
|
656
|
+
import * as p4 from "@clack/prompts";
|
|
657
|
+
|
|
658
|
+
// src/auth/read-auth.ts
|
|
659
|
+
import fsExtra13 from "fs-extra";
|
|
660
|
+
|
|
661
|
+
// src/auth/auth-path.ts
|
|
662
|
+
import path11 from "path";
|
|
663
|
+
import os from "os";
|
|
664
|
+
function authPath() {
|
|
665
|
+
return path11.join(os.homedir(), ".impulselab", "auth.json");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/schemas/auth-credentials.ts
|
|
669
|
+
import { z as z6 } from "zod";
|
|
670
|
+
var AuthCredentialsSchema = z6.object({
|
|
671
|
+
token: z6.string(),
|
|
672
|
+
refreshToken: z6.string().optional(),
|
|
673
|
+
expiresAt: z6.string().optional(),
|
|
674
|
+
email: z6.string()
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// src/auth/read-auth.ts
|
|
678
|
+
var { readJson: readJson5, pathExists: pathExists12 } = fsExtra13;
|
|
679
|
+
async function readAuth() {
|
|
680
|
+
const file = authPath();
|
|
681
|
+
if (!await pathExists12(file)) return null;
|
|
682
|
+
let raw;
|
|
683
|
+
try {
|
|
684
|
+
raw = await readJson5(file);
|
|
685
|
+
} catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const parsed = AuthCredentialsSchema.safeParse(raw);
|
|
689
|
+
if (!parsed.success) return null;
|
|
690
|
+
return parsed.data;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/auth/require-auth.ts
|
|
694
|
+
async function requireAuth() {
|
|
695
|
+
const credentials = await readAuth();
|
|
696
|
+
if (!credentials) {
|
|
697
|
+
p4.cancel("Not authenticated. Run `impulse login` first.");
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
return credentials;
|
|
701
|
+
}
|
|
702
|
+
|
|
655
703
|
// src/commands/add.ts
|
|
656
704
|
async function resolveModuleDeps(moduleId, localPath, resolved, orderedModules) {
|
|
657
705
|
if (resolved.has(moduleId)) return;
|
|
@@ -670,23 +718,27 @@ async function resolveWithParent(moduleId, localPath, installedNames, resolved,
|
|
|
670
718
|
await resolveModuleDeps(moduleId, localPath, resolved, orderedModules);
|
|
671
719
|
}
|
|
672
720
|
function detectPackageManager(cwd) {
|
|
673
|
-
if (existsSync(
|
|
674
|
-
if (existsSync(
|
|
721
|
+
if (existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
722
|
+
if (existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
|
|
675
723
|
return "npm";
|
|
676
724
|
}
|
|
725
|
+
function isPnpmMonorepo(cwd) {
|
|
726
|
+
return existsSync(path12.join(cwd, "pnpm-workspace.yaml"));
|
|
727
|
+
}
|
|
677
728
|
function installNpmDeps(deps, cwd, dryRun) {
|
|
678
729
|
if (deps.length === 0) return;
|
|
679
730
|
const pm = detectPackageManager(cwd);
|
|
680
|
-
const
|
|
731
|
+
const isMonorepo = pm === "pnpm" && isPnpmMonorepo(cwd);
|
|
732
|
+
const args = isMonorepo ? ["-w", "add", ...deps] : ["add", ...deps];
|
|
681
733
|
if (dryRun) {
|
|
682
|
-
|
|
734
|
+
p5.log.info(`[dry-run] Would run: ${pm} ${args.join(" ")}`);
|
|
683
735
|
return;
|
|
684
736
|
}
|
|
685
|
-
|
|
737
|
+
p5.log.step(`Installing dependencies: ${deps.join(", ")}`);
|
|
686
738
|
execFileSync(pm, args, { cwd, stdio: "inherit" });
|
|
687
739
|
}
|
|
688
740
|
async function installModule(moduleId, manifest, cwd, dryRun, localPath) {
|
|
689
|
-
|
|
741
|
+
p5.log.step(`Installing ${moduleId}@${manifest.version}...`);
|
|
690
742
|
if (manifest.files.length > 0) {
|
|
691
743
|
const installed = await installFiles({
|
|
692
744
|
moduleName: moduleId,
|
|
@@ -697,11 +749,11 @@ async function installModule(moduleId, manifest, cwd, dryRun, localPath) {
|
|
|
697
749
|
});
|
|
698
750
|
for (const f of installed) {
|
|
699
751
|
const icon = f.action === "created" || f.action === "would-create" ? "+" : f.action === "overwritten" || f.action === "would-overwrite" ? "~" : "=";
|
|
700
|
-
|
|
752
|
+
p5.log.message(` ${icon} ${f.dest}`);
|
|
701
753
|
}
|
|
702
754
|
}
|
|
703
755
|
for (const transform of manifest.transforms) {
|
|
704
|
-
|
|
756
|
+
p5.log.step(` transform: ${transform.type} \u2192 ${transform.target}`);
|
|
705
757
|
await runTransform(transform, cwd, dryRun);
|
|
706
758
|
}
|
|
707
759
|
}
|
|
@@ -719,48 +771,125 @@ function recordModule(config, moduleId, manifest, now) {
|
|
|
719
771
|
config.installedModules.push(record);
|
|
720
772
|
}
|
|
721
773
|
}
|
|
774
|
+
async function pickModulesInteractively(localPath) {
|
|
775
|
+
const s = p5.spinner();
|
|
776
|
+
s.start("Loading available modules...");
|
|
777
|
+
let modules;
|
|
778
|
+
try {
|
|
779
|
+
modules = await listAvailableModules(localPath);
|
|
780
|
+
} catch (err) {
|
|
781
|
+
s.stop("Failed to load modules.");
|
|
782
|
+
throw err;
|
|
783
|
+
}
|
|
784
|
+
s.stop("Modules loaded.");
|
|
785
|
+
if (modules.length === 0) {
|
|
786
|
+
p5.log.warn("No modules available.");
|
|
787
|
+
return [];
|
|
788
|
+
}
|
|
789
|
+
const options = [];
|
|
790
|
+
for (const mod of modules) {
|
|
791
|
+
options.push({
|
|
792
|
+
value: mod.name,
|
|
793
|
+
label: mod.name,
|
|
794
|
+
...mod.description !== void 0 ? { hint: mod.description } : {}
|
|
795
|
+
});
|
|
796
|
+
for (const sub of mod.subModules ?? []) {
|
|
797
|
+
options.push({
|
|
798
|
+
value: `${mod.name}/${sub}`,
|
|
799
|
+
label: ` \u21B3 ${sub}`,
|
|
800
|
+
hint: `sub-module of ${mod.name}`
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const selected = await p5.multiselect({
|
|
805
|
+
message: "Select modules to install:",
|
|
806
|
+
options,
|
|
807
|
+
required: true
|
|
808
|
+
});
|
|
809
|
+
if (p5.isCancel(selected)) {
|
|
810
|
+
p5.outro("Cancelled.");
|
|
811
|
+
process.exit(0);
|
|
812
|
+
}
|
|
813
|
+
const result = new Set(selected);
|
|
814
|
+
for (const id of selected) {
|
|
815
|
+
const { parent, child } = parseModuleId(id);
|
|
816
|
+
if (child !== null) {
|
|
817
|
+
result.add(parent);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return [...result];
|
|
821
|
+
}
|
|
722
822
|
async function runAdd(options) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
823
|
+
let { moduleNames, cwd, dryRun, localPath, withSubModules = [] } = options;
|
|
824
|
+
let allTargets;
|
|
825
|
+
if (moduleNames.length === 0) {
|
|
826
|
+
p5.intro(`impulse add${dryRun ? " [dry-run]" : ""}`);
|
|
827
|
+
await requireAuth();
|
|
828
|
+
if (withSubModules.length > 0) {
|
|
829
|
+
p5.log.warn(`--with is ignored in interactive mode. Select sub-modules from the picker.`);
|
|
830
|
+
withSubModules = [];
|
|
831
|
+
}
|
|
832
|
+
const picked = await pickModulesInteractively(localPath);
|
|
833
|
+
if (picked.length === 0) {
|
|
834
|
+
p5.outro("Nothing selected.");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
allTargets = picked;
|
|
838
|
+
} else {
|
|
839
|
+
if (moduleNames.length > 1 && withSubModules.length > 0) {
|
|
840
|
+
p5.log.warn(
|
|
841
|
+
`--with is ignored when multiple modules are provided. Specify a single module to use --with.`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const withIds = moduleNames.length === 1 && withSubModules.length > 0 ? withSubModules.map((sub) => `${moduleNames[0]}/${sub}`) : [];
|
|
845
|
+
allTargets = [...moduleNames, ...withIds];
|
|
846
|
+
p5.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
|
|
847
|
+
await requireAuth();
|
|
848
|
+
}
|
|
727
849
|
const config = await readConfig(cwd);
|
|
728
850
|
if (!config) {
|
|
729
|
-
|
|
851
|
+
p5.cancel("No .impulse.json found. Run `impulse init` first.");
|
|
730
852
|
process.exit(1);
|
|
731
853
|
}
|
|
732
854
|
const installedNames = new Set(config.installedModules.map((m) => m.name));
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const reinstall = await p4.confirm({
|
|
738
|
-
message: "Reinstall?",
|
|
739
|
-
initialValue: false
|
|
855
|
+
if (withSubModules.length === 0) {
|
|
856
|
+
const alreadyInstalled = allTargets.filter((id) => {
|
|
857
|
+
const { child } = parseModuleId(id);
|
|
858
|
+
return child === null && installedNames.has(id);
|
|
740
859
|
});
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
|
|
860
|
+
if (alreadyInstalled.length > 0) {
|
|
861
|
+
for (const id of alreadyInstalled) {
|
|
862
|
+
const existing = config.installedModules.find((m) => m.name === id);
|
|
863
|
+
p5.log.warn(`Module "${id}" is already installed (v${existing?.version ?? "?"}).`);
|
|
864
|
+
}
|
|
865
|
+
const reinstall = await p5.confirm({
|
|
866
|
+
message: alreadyInstalled.length === 1 ? "Reinstall?" : "Reinstall all?",
|
|
867
|
+
initialValue: false
|
|
868
|
+
});
|
|
869
|
+
if (p5.isCancel(reinstall) || !reinstall) {
|
|
870
|
+
p5.outro("Cancelled.");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
744
873
|
}
|
|
745
874
|
}
|
|
746
|
-
if (withSubModules.length > 0) {
|
|
747
|
-
const parentManifest = await fetchModuleManifest(
|
|
875
|
+
if (moduleNames.length === 1 && withSubModules.length > 0) {
|
|
876
|
+
const parentManifest = await fetchModuleManifest(moduleNames[0], localPath).catch(() => null);
|
|
748
877
|
if (parentManifest) {
|
|
749
878
|
if (parentManifest.subModules.length === 0) {
|
|
750
|
-
|
|
879
|
+
p5.cancel(`"${moduleNames[0]}" has no declared sub-modules.`);
|
|
751
880
|
process.exit(1);
|
|
752
881
|
}
|
|
753
882
|
const invalid = withSubModules.filter((sub) => !parentManifest.subModules.includes(sub));
|
|
754
883
|
if (invalid.length > 0) {
|
|
755
|
-
|
|
756
|
-
`Unknown sub-module(s) for "${
|
|
884
|
+
p5.cancel(
|
|
885
|
+
`Unknown sub-module(s) for "${moduleNames[0]}": ${invalid.join(", ")}.
|
|
757
886
|
Available: ${parentManifest.subModules.join(", ")}`
|
|
758
887
|
);
|
|
759
888
|
process.exit(1);
|
|
760
889
|
}
|
|
761
890
|
}
|
|
762
891
|
}
|
|
763
|
-
const s =
|
|
892
|
+
const s = p5.spinner();
|
|
764
893
|
s.start("Resolving dependencies...");
|
|
765
894
|
const resolved = /* @__PURE__ */ new Set();
|
|
766
895
|
const orderedModules = [];
|
|
@@ -770,7 +899,7 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
770
899
|
}
|
|
771
900
|
} catch (err) {
|
|
772
901
|
s.stop("Dependency resolution failed.");
|
|
773
|
-
|
|
902
|
+
p5.cancel(err instanceof Error ? err.message : String(err));
|
|
774
903
|
process.exit(1);
|
|
775
904
|
}
|
|
776
905
|
s.stop(`Resolved: ${orderedModules.join(" \u2192 ")}`);
|
|
@@ -784,15 +913,15 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
784
913
|
allDeps.add(dep);
|
|
785
914
|
}
|
|
786
915
|
}
|
|
787
|
-
|
|
916
|
+
p5.log.message("\nSummary of changes:");
|
|
788
917
|
for (const [id, manifest] of manifests) {
|
|
789
|
-
|
|
918
|
+
p5.log.message(`
|
|
790
919
|
Module: ${id}@${manifest.version}`);
|
|
791
920
|
for (const file of manifest.files) {
|
|
792
|
-
|
|
921
|
+
p5.log.message(` + ${file.dest}`);
|
|
793
922
|
}
|
|
794
923
|
for (const transform of manifest.transforms) {
|
|
795
|
-
|
|
924
|
+
p5.log.message(` ~ ${transform.type} \u2192 ${transform.target}`);
|
|
796
925
|
}
|
|
797
926
|
}
|
|
798
927
|
const allEnvVars = /* @__PURE__ */ new Set();
|
|
@@ -802,20 +931,20 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
802
931
|
}
|
|
803
932
|
}
|
|
804
933
|
if (allDeps.size > 0) {
|
|
805
|
-
|
|
934
|
+
p5.log.message(`
|
|
806
935
|
npm deps: ${[...allDeps].join(", ")}`);
|
|
807
936
|
}
|
|
808
937
|
if (allEnvVars.size > 0) {
|
|
809
|
-
|
|
938
|
+
p5.log.message(`
|
|
810
939
|
env vars required: ${[...allEnvVars].join(", ")}`);
|
|
811
940
|
}
|
|
812
941
|
if (!dryRun) {
|
|
813
|
-
const
|
|
942
|
+
const confirm6 = await p5.confirm({
|
|
814
943
|
message: "Proceed?",
|
|
815
944
|
initialValue: true
|
|
816
945
|
});
|
|
817
|
-
if (
|
|
818
|
-
|
|
946
|
+
if (p5.isCancel(confirm6) || !confirm6) {
|
|
947
|
+
p5.outro("Cancelled.");
|
|
819
948
|
return;
|
|
820
949
|
}
|
|
821
950
|
}
|
|
@@ -824,7 +953,7 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
824
953
|
const targetModules = orderedModules.filter((id) => primaryTargetSet.has(id));
|
|
825
954
|
const depPostInstallHooks = [];
|
|
826
955
|
if (depModules.length > 0) {
|
|
827
|
-
|
|
956
|
+
p5.log.step(`Installing module dependencies: ${depModules.join(", ")}`);
|
|
828
957
|
for (const dep of depModules) {
|
|
829
958
|
const depManifest = manifests.get(dep);
|
|
830
959
|
if (!depManifest) continue;
|
|
@@ -842,9 +971,9 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
842
971
|
installNpmDeps([...allDeps], cwd, dryRun);
|
|
843
972
|
if (!dryRun) {
|
|
844
973
|
for (const { name, hooks } of depPostInstallHooks) {
|
|
845
|
-
|
|
974
|
+
p5.log.step(`Running post-install hooks for ${name}...`);
|
|
846
975
|
for (const hook of hooks) {
|
|
847
|
-
|
|
976
|
+
p5.log.message(` $ ${hook}`);
|
|
848
977
|
execSync2(hook, { cwd, stdio: "inherit" });
|
|
849
978
|
}
|
|
850
979
|
}
|
|
@@ -853,9 +982,9 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
853
982
|
for (const targetId of targetModules) {
|
|
854
983
|
const targetManifest = manifests.get(targetId);
|
|
855
984
|
if (!targetManifest?.postInstall?.length) continue;
|
|
856
|
-
|
|
985
|
+
p5.log.step(`Running post-install hooks for ${targetId}...`);
|
|
857
986
|
for (const hook of targetManifest.postInstall) {
|
|
858
|
-
|
|
987
|
+
p5.log.message(` $ ${hook}`);
|
|
859
988
|
execSync2(hook, { cwd, stdio: "inherit" });
|
|
860
989
|
}
|
|
861
990
|
}
|
|
@@ -873,43 +1002,44 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
873
1002
|
await writeConfig(config, cwd);
|
|
874
1003
|
}
|
|
875
1004
|
const label = allTargets.length === 1 ? `"${allTargets[0]}"` : allTargets.map((t) => `"${t}"`).join(", ");
|
|
876
|
-
|
|
1005
|
+
p5.outro(
|
|
877
1006
|
dryRun ? "Dry run complete \u2014 no files were modified." : `Module(s) ${label} installed successfully!`
|
|
878
1007
|
);
|
|
879
1008
|
}
|
|
880
1009
|
|
|
881
1010
|
// src/commands/list.ts
|
|
882
|
-
import * as
|
|
1011
|
+
import * as p6 from "@clack/prompts";
|
|
883
1012
|
async function runList(options) {
|
|
884
1013
|
const { cwd, localPath } = options;
|
|
885
|
-
|
|
1014
|
+
p6.intro("impulse list");
|
|
1015
|
+
await requireAuth();
|
|
886
1016
|
const config = await readConfig(cwd);
|
|
887
1017
|
const installedNames = new Set(
|
|
888
1018
|
config?.installedModules.map((m) => m.name) ?? []
|
|
889
1019
|
);
|
|
890
|
-
const s =
|
|
1020
|
+
const s = p6.spinner();
|
|
891
1021
|
s.start("Fetching available modules...");
|
|
892
1022
|
let available;
|
|
893
1023
|
try {
|
|
894
1024
|
available = await listAvailableModules(localPath);
|
|
895
1025
|
} catch (err) {
|
|
896
1026
|
s.stop("Failed to fetch module list.");
|
|
897
|
-
|
|
1027
|
+
p6.cancel(err instanceof Error ? err.message : String(err));
|
|
898
1028
|
process.exit(1);
|
|
899
1029
|
}
|
|
900
1030
|
s.stop(`Found ${available.length} module(s).`);
|
|
901
1031
|
if (available.length === 0) {
|
|
902
|
-
|
|
903
|
-
|
|
1032
|
+
p6.log.message("No modules available yet.");
|
|
1033
|
+
p6.outro("Done.");
|
|
904
1034
|
return;
|
|
905
1035
|
}
|
|
906
|
-
|
|
1036
|
+
p6.log.message("\nAvailable modules:\n");
|
|
907
1037
|
for (const mod of available) {
|
|
908
1038
|
const installed = installedNames.has(mod.name);
|
|
909
1039
|
const installedInfo = installed ? config?.installedModules.find((m) => m.name === mod.name) : null;
|
|
910
1040
|
const status = installed ? `[installed v${installedInfo?.version ?? "?"}]` : "[available]";
|
|
911
1041
|
const desc = mod.description ? ` \u2014 ${mod.description}` : "";
|
|
912
|
-
|
|
1042
|
+
p6.log.message(` ${installed ? "\u2713" : "\u25CB"} ${mod.name} ${status}${desc}`);
|
|
913
1043
|
if (mod.subModules && mod.subModules.length > 0) {
|
|
914
1044
|
const last = mod.subModules.length - 1;
|
|
915
1045
|
mod.subModules.forEach((sub, i) => {
|
|
@@ -918,37 +1048,266 @@ async function runList(options) {
|
|
|
918
1048
|
const subInfo = subInstalled ? config?.installedModules.find((m) => m.name === subId) : null;
|
|
919
1049
|
const subStatus = subInstalled ? `[installed v${subInfo?.version ?? "?"}]` : "[not installed]";
|
|
920
1050
|
const connector = i === last ? "\u2514\u2500" : "\u251C\u2500";
|
|
921
|
-
|
|
1051
|
+
p6.log.message(` ${connector} ${sub} ${subStatus}`);
|
|
922
1052
|
});
|
|
923
1053
|
}
|
|
924
1054
|
}
|
|
925
|
-
|
|
1055
|
+
p6.log.message(
|
|
926
1056
|
`
|
|
927
1057
|
Run \`impulse add <module>\` to install a module.`
|
|
928
1058
|
);
|
|
929
|
-
|
|
1059
|
+
p6.log.message(
|
|
930
1060
|
`Run \`impulse add <parent>/<sub>\` or \`impulse add <parent> --with <sub1>,<sub2>\` for sub-modules.`
|
|
931
1061
|
);
|
|
932
|
-
|
|
1062
|
+
p6.outro("Done.");
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/commands/login.ts
|
|
1066
|
+
import * as p7 from "@clack/prompts";
|
|
1067
|
+
|
|
1068
|
+
// src/auth/device-flow.ts
|
|
1069
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1070
|
+
var IMPULSE_BASE_URL = process.env.IMPULSE_BASE_URL ?? "https://impulse.studio";
|
|
1071
|
+
async function requestDeviceCode() {
|
|
1072
|
+
const res = await fetch(`${IMPULSE_BASE_URL}/api/auth/device/code`, {
|
|
1073
|
+
method: "POST",
|
|
1074
|
+
headers: { "Content-Type": "application/json" },
|
|
1075
|
+
body: JSON.stringify({ client: "impulse-cli" })
|
|
1076
|
+
});
|
|
1077
|
+
if (!res.ok) {
|
|
1078
|
+
throw new Error(`Failed to initiate device flow: ${res.status} ${res.statusText}`);
|
|
1079
|
+
}
|
|
1080
|
+
const data = await res.json();
|
|
1081
|
+
assertDeviceCodeResponse(data);
|
|
1082
|
+
return data;
|
|
1083
|
+
}
|
|
1084
|
+
async function pollDeviceToken(deviceCode) {
|
|
1085
|
+
let res;
|
|
1086
|
+
try {
|
|
1087
|
+
res = await fetch(`${IMPULSE_BASE_URL}/api/auth/device/token`, {
|
|
1088
|
+
method: "POST",
|
|
1089
|
+
headers: { "Content-Type": "application/json" },
|
|
1090
|
+
body: JSON.stringify({ deviceCode })
|
|
1091
|
+
});
|
|
1092
|
+
} catch {
|
|
1093
|
+
return { status: "error", message: "Network error while polling for token" };
|
|
1094
|
+
}
|
|
1095
|
+
if (res.status === 200) {
|
|
1096
|
+
const data = await res.json();
|
|
1097
|
+
assertDeviceTokenResponse(data);
|
|
1098
|
+
return { status: "authorized", data };
|
|
1099
|
+
}
|
|
1100
|
+
if (res.status === 202) {
|
|
1101
|
+
return { status: "pending" };
|
|
1102
|
+
}
|
|
1103
|
+
if (res.status === 400) {
|
|
1104
|
+
let body;
|
|
1105
|
+
try {
|
|
1106
|
+
body = await res.json();
|
|
1107
|
+
} catch {
|
|
1108
|
+
body = {};
|
|
1109
|
+
}
|
|
1110
|
+
const error = typeof body === "object" && body !== null && "error" in body ? String(body.error) : "";
|
|
1111
|
+
if (error === "slow_down") return { status: "slow_down" };
|
|
1112
|
+
if (error === "expired" || error === "authorization_expired") return { status: "expired" };
|
|
1113
|
+
return { status: "error", message: error || "Device code error" };
|
|
1114
|
+
}
|
|
1115
|
+
return { status: "error", message: `Unexpected response: ${res.status}` };
|
|
1116
|
+
}
|
|
1117
|
+
function openBrowser(url) {
|
|
1118
|
+
try {
|
|
1119
|
+
const platform = process.platform;
|
|
1120
|
+
if (platform === "darwin") {
|
|
1121
|
+
execFileSync2("open", [url]);
|
|
1122
|
+
} else if (platform === "win32") {
|
|
1123
|
+
execFileSync2("cmd", ["/c", "start", url]);
|
|
1124
|
+
} else {
|
|
1125
|
+
execFileSync2("xdg-open", [url]);
|
|
1126
|
+
}
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
function assertDeviceCodeResponse(data) {
|
|
1131
|
+
if (typeof data !== "object" || data === null || typeof data.deviceCode !== "string" || typeof data.userCode !== "string" || typeof data.verificationUri !== "string") {
|
|
1132
|
+
throw new Error("Invalid device code response from server");
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function assertDeviceTokenResponse(data) {
|
|
1136
|
+
if (typeof data !== "object" || data === null || typeof data.token !== "string" || typeof data.email !== "string") {
|
|
1137
|
+
throw new Error("Invalid token response from server");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/auth/write-auth.ts
|
|
1142
|
+
import fsExtra14 from "fs-extra";
|
|
1143
|
+
import { chmod } from "fs/promises";
|
|
1144
|
+
var { outputJson } = fsExtra14;
|
|
1145
|
+
async function writeAuth(credentials) {
|
|
1146
|
+
const file = authPath();
|
|
1147
|
+
await outputJson(file, credentials, { spaces: 2, mode: 384 });
|
|
1148
|
+
await chmod(file, 384);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/commands/login.ts
|
|
1152
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1153
|
+
var MAX_POLL_DURATION_MS = 5 * 60 * 1e3;
|
|
1154
|
+
async function runLogin() {
|
|
1155
|
+
p7.intro("impulse login");
|
|
1156
|
+
const existing = await readAuth();
|
|
1157
|
+
if (existing) {
|
|
1158
|
+
const reauth = await p7.confirm({
|
|
1159
|
+
message: `Already logged in as ${existing.email}. Log in again?`,
|
|
1160
|
+
initialValue: false
|
|
1161
|
+
});
|
|
1162
|
+
if (p7.isCancel(reauth) || !reauth) {
|
|
1163
|
+
p7.outro("Cancelled.");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const s = p7.spinner();
|
|
1168
|
+
s.start("Initiating login...");
|
|
1169
|
+
let deviceCode;
|
|
1170
|
+
try {
|
|
1171
|
+
deviceCode = await requestDeviceCode();
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
s.stop("Failed to initiate login.");
|
|
1174
|
+
p7.cancel(err instanceof Error ? err.message : String(err));
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
s.stop("Opening browser for authentication...");
|
|
1178
|
+
const browserUrl = deviceCode.verificationUri;
|
|
1179
|
+
p7.log.message(`
|
|
1180
|
+
Visit the following URL to authenticate:
|
|
1181
|
+
${browserUrl}
|
|
1182
|
+
`);
|
|
1183
|
+
p7.log.message(`Your one-time code: ${deviceCode.userCode}
|
|
1184
|
+
`);
|
|
1185
|
+
openBrowser(browserUrl);
|
|
1186
|
+
p7.log.info(`If the browser did not open, copy the URL above.
|
|
1187
|
+
Base URL: ${IMPULSE_BASE_URL}`);
|
|
1188
|
+
const pollInterval = Math.max(
|
|
1189
|
+
(deviceCode.interval ?? 5) * 1e3,
|
|
1190
|
+
DEFAULT_POLL_INTERVAL_MS
|
|
1191
|
+
);
|
|
1192
|
+
const expiresAt = Date.now() + (deviceCode.expiresIn ?? 300) * 1e3;
|
|
1193
|
+
const deadline = Math.min(expiresAt, Date.now() + MAX_POLL_DURATION_MS);
|
|
1194
|
+
const pollSpinner = p7.spinner();
|
|
1195
|
+
pollSpinner.start("Waiting for authentication...");
|
|
1196
|
+
let currentInterval = pollInterval;
|
|
1197
|
+
while (Date.now() < deadline) {
|
|
1198
|
+
await sleep(currentInterval);
|
|
1199
|
+
const result = await pollDeviceToken(deviceCode.deviceCode);
|
|
1200
|
+
if (result.status === "authorized") {
|
|
1201
|
+
pollSpinner.stop("Authentication successful!");
|
|
1202
|
+
await writeAuth({
|
|
1203
|
+
token: result.data.token,
|
|
1204
|
+
refreshToken: result.data.refreshToken,
|
|
1205
|
+
expiresAt: result.data.expiresAt,
|
|
1206
|
+
email: result.data.email
|
|
1207
|
+
});
|
|
1208
|
+
p7.outro(`Logged in as ${result.data.email}`);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (result.status === "slow_down") {
|
|
1212
|
+
currentInterval += 5e3;
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
if (result.status === "expired") {
|
|
1216
|
+
pollSpinner.stop("Device code expired.");
|
|
1217
|
+
p7.cancel("Authentication timed out. Run `impulse login` again.");
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
if (result.status === "error") {
|
|
1221
|
+
pollSpinner.stop("Authentication failed.");
|
|
1222
|
+
p7.cancel(result.message);
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
pollSpinner.stop("Authentication timed out.");
|
|
1227
|
+
p7.cancel("Authentication timed out. Run `impulse login` again.");
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
function sleep(ms) {
|
|
1231
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/commands/logout.ts
|
|
1235
|
+
import * as p8 from "@clack/prompts";
|
|
1236
|
+
|
|
1237
|
+
// src/auth/clear-auth.ts
|
|
1238
|
+
import fsExtra15 from "fs-extra";
|
|
1239
|
+
var { remove, pathExists: pathExists13 } = fsExtra15;
|
|
1240
|
+
async function clearAuth() {
|
|
1241
|
+
const file = authPath();
|
|
1242
|
+
if (!await pathExists13(file)) return false;
|
|
1243
|
+
await remove(file);
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/commands/logout.ts
|
|
1248
|
+
async function runLogout() {
|
|
1249
|
+
p8.intro("impulse logout");
|
|
1250
|
+
const credentials = await readAuth();
|
|
1251
|
+
if (!credentials) {
|
|
1252
|
+
p8.outro("Not currently logged in.");
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const confirm6 = await p8.confirm({
|
|
1256
|
+
message: `Log out ${credentials.email}?`,
|
|
1257
|
+
initialValue: true
|
|
1258
|
+
});
|
|
1259
|
+
if (p8.isCancel(confirm6) || !confirm6) {
|
|
1260
|
+
p8.outro("Cancelled.");
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
await clearAuth();
|
|
1264
|
+
p8.outro("Logged out successfully.");
|
|
933
1265
|
}
|
|
934
1266
|
|
|
1267
|
+
// src/commands/whoami.ts
|
|
1268
|
+
import * as p9 from "@clack/prompts";
|
|
1269
|
+
async function runWhoami() {
|
|
1270
|
+
p9.intro("impulse whoami");
|
|
1271
|
+
const credentials = await readAuth();
|
|
1272
|
+
if (!credentials) {
|
|
1273
|
+
p9.cancel("Not authenticated. Run `impulse login` first.");
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
p9.log.message(`Logged in as: ${credentials.email}`);
|
|
1277
|
+
if (credentials.expiresAt) {
|
|
1278
|
+
const expires = new Date(credentials.expiresAt);
|
|
1279
|
+
const now = /* @__PURE__ */ new Date();
|
|
1280
|
+
if (expires < now) {
|
|
1281
|
+
p9.log.warn("Your session has expired. Run `impulse login` to re-authenticate.");
|
|
1282
|
+
} else {
|
|
1283
|
+
p9.log.message(`Session expires: ${expires.toLocaleString()}`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
p9.outro("Done.");
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/cli-version.ts
|
|
1290
|
+
import { createRequire } from "module";
|
|
1291
|
+
var require2 = createRequire(import.meta.url);
|
|
1292
|
+
var CLI_VERSION = require2("../package.json").version;
|
|
1293
|
+
|
|
935
1294
|
// src/index.ts
|
|
936
1295
|
var program = new Command();
|
|
937
|
-
program.name("impulse").description("ImpulseLab CLI \u2014 install and manage modules for your projects").version(
|
|
1296
|
+
program.name("impulse").description("ImpulseLab CLI \u2014 install and manage modules for your projects").version(CLI_VERSION);
|
|
938
1297
|
program.command("init").description("Initialize impulse in the current project").option("--force", "Reinitialize even if .impulse.json already exists", false).action(async (options) => {
|
|
939
1298
|
await runInit({ cwd: process.cwd(), force: options.force });
|
|
940
1299
|
});
|
|
941
|
-
program.command("add
|
|
942
|
-
"Add
|
|
1300
|
+
program.command("add [modules...]").description(
|
|
1301
|
+
"Add module(s) to the current project.\n No args: opens interactive multi-select picker\n Single module: `add auth`\n Multiple modules: `add auth attio/gocardless`\n Sub-module syntax: `add attio/quote-to-cash`\n Use --with to install multiple sub-modules at once: `add attio --with quote-to-cash,gocardless`"
|
|
943
1302
|
).option("--dry-run", "Preview changes without writing files", false).option(
|
|
944
1303
|
"--local <path>",
|
|
945
1304
|
"Use a local modules directory (for development)"
|
|
946
1305
|
).option(
|
|
947
1306
|
"--with <submodules>",
|
|
948
1307
|
"Comma-separated sub-modules to install alongside the parent (e.g. --with quote-to-cash,gocardless)"
|
|
949
|
-
).action(async (
|
|
1308
|
+
).action(async (modules, options) => {
|
|
950
1309
|
const addOpts = {
|
|
951
|
-
|
|
1310
|
+
moduleNames: modules ?? [],
|
|
952
1311
|
cwd: process.cwd(),
|
|
953
1312
|
dryRun: options.dryRun
|
|
954
1313
|
};
|
|
@@ -966,4 +1325,13 @@ program.command("list").description("List available and installed modules").opti
|
|
|
966
1325
|
if (options.local !== void 0) listOpts.localPath = options.local;
|
|
967
1326
|
await runList(listOpts);
|
|
968
1327
|
});
|
|
1328
|
+
program.command("login").description("Authenticate with ImpulseLab (opens browser for device flow)").action(async () => {
|
|
1329
|
+
await runLogin();
|
|
1330
|
+
});
|
|
1331
|
+
program.command("logout").description("Log out and clear stored credentials").action(async () => {
|
|
1332
|
+
await runLogout();
|
|
1333
|
+
});
|
|
1334
|
+
program.command("whoami").description("Show the currently authenticated user").action(async () => {
|
|
1335
|
+
await runWhoami();
|
|
1336
|
+
});
|
|
969
1337
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impulselab/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "ImpulseLab CLI — install and manage modules for your projects",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"directory": "cli"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
+
"bumpp": "^10.3.2",
|
|
23
24
|
"@types/fs-extra": "^11.0.4",
|
|
24
25
|
"@types/node": "^20.0.0",
|
|
25
26
|
"oxlint": "^0.13.0",
|
|
@@ -37,6 +38,8 @@
|
|
|
37
38
|
"build": "tsup src/index.ts --format esm --target es2022 --dts",
|
|
38
39
|
"dev": "tsup src/index.ts --format esm --target es2022 --watch",
|
|
39
40
|
"lint": "oxlint .",
|
|
40
|
-
"test": "vitest run"
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"release:publish": "pnpm run test && pnpm run build && pnpm publish --no-git-checks --access public",
|
|
43
|
+
"release": "bumpp"
|
|
41
44
|
}
|
|
42
45
|
}
|