@impulselab/cli 0.1.4 → 0.1.5

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 +181 -87
  2. 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(z5.string()).default([]),
249
+ moduleDependencies: z5.array(ModuleDependencySchema).default([]),
250
250
  /** Optional module dependencies that enhance this module but are not required. */
251
- optionalModuleDependencies: z5.array(z5.string()).default([]),
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/${GITHUB_ORG}/${GITHUB_REPO}/${GITHUB_BRANCH}/${MODULES_DIR}/${registryPath}/${file}`,
275
- moduleList: () => `https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/contents/${MODULES_DIR}`,
276
- subModulesList: (parentModule) => `https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/contents/${MODULES_DIR}/${parentModule}/sub-modules`
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}/sub-modules/${child}`;
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, "module.json");
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, "module.json");
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(localPath, entry.name, "module.json");
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(localPath, entry.name, "sub-modules");
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: readFile6 } = await import("fs/promises");
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 readFile6(file, "utf-8");
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 import("fs/promises").then(
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 readFile(file, "utf-8");
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: readFile2, outputFile: outputFile3, pathExists: pathExists8 } = fsExtra9;
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 readFile2(file, "utf-8");
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: readFile3, outputFile: outputFile4, pathExists: pathExists9 } = fsExtra10;
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 readFile3(file, "utf-8");
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: readFile4, outputFile: outputFile5, pathExists: pathExists10 } = fsExtra11;
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 readFile4(file, "utf-8");
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: readFile5, outputFile: outputFile6, pathExists: pathExists11 } = fsExtra12;
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 readFile5(file, "utf-8");
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 resolveModuleDeps(moduleId, localPath, resolved, orderedModules) {
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 fetchModuleManifest(moduleId, localPath);
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
- if (dryRun) {
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 fetchModuleManifest(moduleNames[0], localPath).catch(() => null);
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 manifests = /* @__PURE__ */ new Map();
940
- for (const id of orderedModules) {
941
- manifests.set(id, await fetchModuleManifest(id, localPath));
942
- }
943
- for (const [id, manifest] of manifests) {
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 manifests.values()) {
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 manifests) {
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 manifests.values()) {
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
- if (!dryRun) {
1024
- for (const { name, hooks } of depPostInstallHooks) {
1025
- p5.log.step(`Running post-install hooks for ${name}...`);
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
- p5.log.message(` $ ${hook}`);
1028
- execSync2(hook, { cwd, stdio: "inherit" });
1058
+ allHookLines.push(` [${name}] $ ${hook}`);
1029
1059
  }
1030
1060
  }
1031
- }
1032
- if (!dryRun) {
1033
- for (const targetId of targetModules) {
1034
- const targetManifest = manifests.get(targetId);
1035
- if (!targetManifest?.postInstall?.length) continue;
1036
- p5.log.step(`Running post-install hooks for ${targetId}...`);
1037
- for (const hook of targetManifest.postInstall) {
1038
- p5.log.message(` $ ${hook}`);
1039
- execSync2(hook, { cwd, stdio: "inherit" });
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://impulse.studio";
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({ client: "impulse-cli" })
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({ deviceCode })
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 === "expired" || error === "authorization_expired") return { status: "expired" };
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.deviceCode !== "string" || typeof data.userCode !== "string" || typeof data.verificationUri !== "string") {
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.token !== "string" || typeof data.email !== "string") {
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
- refreshToken: result.data.refreshToken,
1287
- expiresAt: result.data.expiresAt,
1370
+ expiresAt: expiresAt2,
1288
1371
  email: result.data.email
1289
1372
  });
1290
- p7.outro(`Logged in as ${result.data.email}`);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impulselab/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "ImpulseLab CLI — install and manage modules for your projects",
5
5
  "private": false,
6
6
  "type": "module",