@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.
Files changed (2) hide show
  1. package/dist/index.js +432 -64
  2. 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 path11 from "path";
201
- import * as p4 from "@clack/prompts";
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(path11.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
674
- if (existsSync(path11.join(cwd, "yarn.lock"))) return "yarn";
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 args = ["add", ...deps];
731
+ const isMonorepo = pm === "pnpm" && isPnpmMonorepo(cwd);
732
+ const args = isMonorepo ? ["-w", "add", ...deps] : ["add", ...deps];
681
733
  if (dryRun) {
682
- p4.log.info(`[dry-run] Would run: ${pm} ${args.join(" ")}`);
734
+ p5.log.info(`[dry-run] Would run: ${pm} ${args.join(" ")}`);
683
735
  return;
684
736
  }
685
- p4.log.step(`Installing dependencies: ${deps.join(", ")}`);
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
- p4.log.step(`Installing ${moduleId}@${manifest.version}...`);
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
- p4.log.message(` ${icon} ${f.dest}`);
752
+ p5.log.message(` ${icon} ${f.dest}`);
701
753
  }
702
754
  }
703
755
  for (const transform of manifest.transforms) {
704
- p4.log.step(` transform: ${transform.type} \u2192 ${transform.target}`);
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
- const { moduleName, cwd, dryRun, localPath, withSubModules = [] } = options;
724
- const withIds = withSubModules.map((sub) => `${moduleName}/${sub}`);
725
- const allTargets = [moduleName, ...withIds];
726
- p4.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
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
- p4.cancel("No .impulse.json found. Run `impulse init` first.");
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
- const { child: mainChild } = parseModuleId(moduleName);
734
- if (mainChild === null && withSubModules.length === 0 && installedNames.has(moduleName)) {
735
- const existing = config.installedModules.find((m) => m.name === moduleName);
736
- p4.log.warn(`Module "${moduleName}" is already installed (v${existing?.version ?? "?"}).`);
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 (p4.isCancel(reinstall) || !reinstall) {
742
- p4.outro("Cancelled.");
743
- return;
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(moduleName, localPath).catch(() => null);
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
- p4.cancel(`"${moduleName}" has no declared sub-modules.`);
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
- p4.cancel(
756
- `Unknown sub-module(s) for "${moduleName}": ${invalid.join(", ")}.
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 = p4.spinner();
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
- p4.cancel(err instanceof Error ? err.message : String(err));
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
- p4.log.message("\nSummary of changes:");
916
+ p5.log.message("\nSummary of changes:");
788
917
  for (const [id, manifest] of manifests) {
789
- p4.log.message(`
918
+ p5.log.message(`
790
919
  Module: ${id}@${manifest.version}`);
791
920
  for (const file of manifest.files) {
792
- p4.log.message(` + ${file.dest}`);
921
+ p5.log.message(` + ${file.dest}`);
793
922
  }
794
923
  for (const transform of manifest.transforms) {
795
- p4.log.message(` ~ ${transform.type} \u2192 ${transform.target}`);
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
- p4.log.message(`
934
+ p5.log.message(`
806
935
  npm deps: ${[...allDeps].join(", ")}`);
807
936
  }
808
937
  if (allEnvVars.size > 0) {
809
- p4.log.message(`
938
+ p5.log.message(`
810
939
  env vars required: ${[...allEnvVars].join(", ")}`);
811
940
  }
812
941
  if (!dryRun) {
813
- const confirm4 = await p4.confirm({
942
+ const confirm6 = await p5.confirm({
814
943
  message: "Proceed?",
815
944
  initialValue: true
816
945
  });
817
- if (p4.isCancel(confirm4) || !confirm4) {
818
- p4.outro("Cancelled.");
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
- p4.log.step(`Installing module dependencies: ${depModules.join(", ")}`);
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
- p4.log.step(`Running post-install hooks for ${name}...`);
974
+ p5.log.step(`Running post-install hooks for ${name}...`);
846
975
  for (const hook of hooks) {
847
- p4.log.message(` $ ${hook}`);
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
- p4.log.step(`Running post-install hooks for ${targetId}...`);
985
+ p5.log.step(`Running post-install hooks for ${targetId}...`);
857
986
  for (const hook of targetManifest.postInstall) {
858
- p4.log.message(` $ ${hook}`);
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
- p4.outro(
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 p5 from "@clack/prompts";
1011
+ import * as p6 from "@clack/prompts";
883
1012
  async function runList(options) {
884
1013
  const { cwd, localPath } = options;
885
- p5.intro("impulse list");
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 = p5.spinner();
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
- p5.cancel(err instanceof Error ? err.message : String(err));
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
- p5.log.message("No modules available yet.");
903
- p5.outro("Done.");
1032
+ p6.log.message("No modules available yet.");
1033
+ p6.outro("Done.");
904
1034
  return;
905
1035
  }
906
- p5.log.message("\nAvailable modules:\n");
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
- p5.log.message(` ${installed ? "\u2713" : "\u25CB"} ${mod.name} ${status}${desc}`);
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
- p5.log.message(` ${connector} ${sub} ${subStatus}`);
1051
+ p6.log.message(` ${connector} ${sub} ${subStatus}`);
922
1052
  });
923
1053
  }
924
1054
  }
925
- p5.log.message(
1055
+ p6.log.message(
926
1056
  `
927
1057
  Run \`impulse add <module>\` to install a module.`
928
1058
  );
929
- p5.log.message(
1059
+ p6.log.message(
930
1060
  `Run \`impulse add <parent>/<sub>\` or \`impulse add <parent> --with <sub1>,<sub2>\` for sub-modules.`
931
1061
  );
932
- p5.outro("Done.");
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("0.1.0");
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 <module>").description(
942
- "Add a module to the current project.\n Supports 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`"
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 (moduleName, options) => {
1308
+ ).action(async (modules, options) => {
950
1309
  const addOpts = {
951
- moduleName,
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.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
  }