@impulselab/cli 0.1.2 → 0.1.4
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 +534 -89
- package/package.json +2 -1
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";
|
|
@@ -211,7 +211,11 @@ import { z as z5 } from "zod";
|
|
|
211
211
|
import { z as z2 } from "zod";
|
|
212
212
|
var ModuleFileSchema = z2.object({
|
|
213
213
|
src: z2.string(),
|
|
214
|
-
dest: z2.string()
|
|
214
|
+
dest: z2.string(),
|
|
215
|
+
/** If set, this file is only installed when the named module is already installed. */
|
|
216
|
+
when: z2.object({
|
|
217
|
+
moduleInstalled: z2.string()
|
|
218
|
+
}).optional()
|
|
215
219
|
});
|
|
216
220
|
|
|
217
221
|
// src/schemas/module-dependency.ts
|
|
@@ -243,11 +247,21 @@ var ModuleManifestSchema = z5.object({
|
|
|
243
247
|
subModules: z5.array(z5.string()).default([]),
|
|
244
248
|
dependencies: z5.array(ModuleDependencySchema).default([]),
|
|
245
249
|
moduleDependencies: z5.array(z5.string()).default([]),
|
|
250
|
+
/** Optional module dependencies that enhance this module but are not required. */
|
|
251
|
+
optionalModuleDependencies: z5.array(z5.string()).default([]),
|
|
246
252
|
files: z5.array(ModuleFileSchema).default([]),
|
|
247
253
|
transforms: z5.array(ModuleTransformSchema).default([]),
|
|
248
254
|
/** Documentation metadata listing env vars this module requires (displayed in install summary). */
|
|
249
255
|
envVars: z5.array(z5.string()).default([]),
|
|
250
|
-
postInstall: z5.array(z5.string()).optional()
|
|
256
|
+
postInstall: z5.array(z5.string()).optional(),
|
|
257
|
+
/** Logical category for grouping in `impulse list`. */
|
|
258
|
+
category: z5.enum(["core", "feature", "integration", "dx"]).optional(),
|
|
259
|
+
/** Module names that cannot be installed alongside this module. */
|
|
260
|
+
incompatibleWith: z5.array(z5.string()).default([]),
|
|
261
|
+
/** Capability tokens this module provides (for future dependency resolution). */
|
|
262
|
+
provides: z5.array(z5.string()).default([]),
|
|
263
|
+
/** Which package in a monorepo this module targets. */
|
|
264
|
+
targetPackage: z5.enum(["database", "server", "web", "root"]).default("root")
|
|
251
265
|
});
|
|
252
266
|
|
|
253
267
|
// src/registry/github-urls.ts
|
|
@@ -364,7 +378,8 @@ async function listAvailableModules(localPath) {
|
|
|
364
378
|
result.push({
|
|
365
379
|
name: parsed.data.name,
|
|
366
380
|
description: parsed.data.description,
|
|
367
|
-
...subModules ? { subModules } : {}
|
|
381
|
+
...subModules ? { subModules } : {},
|
|
382
|
+
...parsed.data.category ? { category: parsed.data.category } : {}
|
|
368
383
|
});
|
|
369
384
|
} catch {
|
|
370
385
|
}
|
|
@@ -432,9 +447,17 @@ import path5 from "path";
|
|
|
432
447
|
import * as p2 from "@clack/prompts";
|
|
433
448
|
var { outputFile, pathExists: pathExists6 } = fsExtra7;
|
|
434
449
|
async function installFiles(options) {
|
|
435
|
-
const { moduleName, files, cwd, dryRun, localPath } = options;
|
|
450
|
+
const { moduleName, files, cwd, dryRun, localPath, installedModules = /* @__PURE__ */ new Set() } = options;
|
|
436
451
|
const results = [];
|
|
437
452
|
for (const file of files) {
|
|
453
|
+
if (file.when?.moduleInstalled !== void 0 && !installedModules.has(file.when.moduleInstalled)) {
|
|
454
|
+
results.push({
|
|
455
|
+
dest: file.dest,
|
|
456
|
+
action: "conditional-skip",
|
|
457
|
+
reason: `requires ${file.when.moduleInstalled}`
|
|
458
|
+
});
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
438
461
|
const destAbs = path5.join(cwd, file.dest);
|
|
439
462
|
const exists = await pathExists6(destAbs);
|
|
440
463
|
if (exists) {
|
|
@@ -652,6 +675,54 @@ async function runTransform(transform, cwd, dryRun) {
|
|
|
652
675
|
}
|
|
653
676
|
}
|
|
654
677
|
|
|
678
|
+
// src/auth/require-auth.ts
|
|
679
|
+
import * as p4 from "@clack/prompts";
|
|
680
|
+
|
|
681
|
+
// src/auth/read-auth.ts
|
|
682
|
+
import fsExtra13 from "fs-extra";
|
|
683
|
+
|
|
684
|
+
// src/auth/auth-path.ts
|
|
685
|
+
import path11 from "path";
|
|
686
|
+
import os from "os";
|
|
687
|
+
function authPath() {
|
|
688
|
+
return path11.join(os.homedir(), ".impulselab", "auth.json");
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/schemas/auth-credentials.ts
|
|
692
|
+
import { z as z6 } from "zod";
|
|
693
|
+
var AuthCredentialsSchema = z6.object({
|
|
694
|
+
token: z6.string(),
|
|
695
|
+
refreshToken: z6.string().optional(),
|
|
696
|
+
expiresAt: z6.string().optional(),
|
|
697
|
+
email: z6.string()
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// src/auth/read-auth.ts
|
|
701
|
+
var { readJson: readJson5, pathExists: pathExists12 } = fsExtra13;
|
|
702
|
+
async function readAuth() {
|
|
703
|
+
const file = authPath();
|
|
704
|
+
if (!await pathExists12(file)) return null;
|
|
705
|
+
let raw;
|
|
706
|
+
try {
|
|
707
|
+
raw = await readJson5(file);
|
|
708
|
+
} catch {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const parsed = AuthCredentialsSchema.safeParse(raw);
|
|
712
|
+
if (!parsed.success) return null;
|
|
713
|
+
return parsed.data;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/auth/require-auth.ts
|
|
717
|
+
async function requireAuth() {
|
|
718
|
+
const credentials = await readAuth();
|
|
719
|
+
if (!credentials) {
|
|
720
|
+
p4.cancel("Not authenticated. Run `impulse login` first.");
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
return credentials;
|
|
724
|
+
}
|
|
725
|
+
|
|
655
726
|
// src/commands/add.ts
|
|
656
727
|
async function resolveModuleDeps(moduleId, localPath, resolved, orderedModules) {
|
|
657
728
|
if (resolved.has(moduleId)) return;
|
|
@@ -670,48 +741,62 @@ async function resolveWithParent(moduleId, localPath, installedNames, resolved,
|
|
|
670
741
|
await resolveModuleDeps(moduleId, localPath, resolved, orderedModules);
|
|
671
742
|
}
|
|
672
743
|
function detectPackageManager(cwd) {
|
|
673
|
-
if (existsSync(
|
|
674
|
-
if (existsSync(
|
|
744
|
+
if (existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
745
|
+
if (existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
|
|
675
746
|
return "npm";
|
|
676
747
|
}
|
|
748
|
+
function isPnpmMonorepo(cwd) {
|
|
749
|
+
return existsSync(path12.join(cwd, "pnpm-workspace.yaml"));
|
|
750
|
+
}
|
|
677
751
|
function installNpmDeps(deps, cwd, dryRun) {
|
|
678
752
|
if (deps.length === 0) return;
|
|
679
753
|
const pm = detectPackageManager(cwd);
|
|
680
|
-
const
|
|
754
|
+
const isMonorepo = pm === "pnpm" && isPnpmMonorepo(cwd);
|
|
755
|
+
const args = isMonorepo ? ["-w", "add", ...deps] : ["add", ...deps];
|
|
681
756
|
if (dryRun) {
|
|
682
|
-
|
|
757
|
+
p5.log.info(`[dry-run] Would run: ${pm} ${args.join(" ")}`);
|
|
683
758
|
return;
|
|
684
759
|
}
|
|
685
|
-
|
|
760
|
+
p5.log.step(`Installing dependencies: ${deps.join(", ")}`);
|
|
686
761
|
execFileSync(pm, args, { cwd, stdio: "inherit" });
|
|
687
762
|
}
|
|
688
|
-
async function installModule(moduleId, manifest, cwd, dryRun, localPath) {
|
|
689
|
-
|
|
763
|
+
async function installModule(moduleId, manifest, cwd, dryRun, installedModules, localPath) {
|
|
764
|
+
p5.log.step(`Installing ${moduleId}@${manifest.version}...`);
|
|
765
|
+
const installedDests = [];
|
|
690
766
|
if (manifest.files.length > 0) {
|
|
691
767
|
const installed = await installFiles({
|
|
692
768
|
moduleName: moduleId,
|
|
693
769
|
files: manifest.files,
|
|
694
770
|
cwd,
|
|
695
771
|
dryRun,
|
|
696
|
-
localPath
|
|
772
|
+
localPath,
|
|
773
|
+
installedModules
|
|
697
774
|
});
|
|
698
775
|
for (const f of installed) {
|
|
776
|
+
if (f.action === "conditional-skip") {
|
|
777
|
+
if (dryRun) {
|
|
778
|
+
p5.log.message(` \u25CB ${f.dest} [skipped: ${f.reason}]`);
|
|
779
|
+
}
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
installedDests.push(f.dest);
|
|
699
783
|
const icon = f.action === "created" || f.action === "would-create" ? "+" : f.action === "overwritten" || f.action === "would-overwrite" ? "~" : "=";
|
|
700
|
-
|
|
784
|
+
p5.log.message(` ${icon} ${f.dest}`);
|
|
701
785
|
}
|
|
702
786
|
}
|
|
703
787
|
for (const transform of manifest.transforms) {
|
|
704
|
-
|
|
788
|
+
p5.log.step(` transform: ${transform.type} \u2192 ${transform.target}`);
|
|
705
789
|
await runTransform(transform, cwd, dryRun);
|
|
706
790
|
}
|
|
791
|
+
return installedDests;
|
|
707
792
|
}
|
|
708
|
-
function recordModule(config, moduleId, manifest, now) {
|
|
793
|
+
function recordModule(config, moduleId, manifest, installedFiles, now) {
|
|
709
794
|
const existing = config.installedModules.findIndex((m) => m.name === moduleId);
|
|
710
795
|
const record = {
|
|
711
796
|
name: moduleId,
|
|
712
797
|
version: manifest.version,
|
|
713
798
|
installedAt: now,
|
|
714
|
-
files:
|
|
799
|
+
files: installedFiles
|
|
715
800
|
};
|
|
716
801
|
if (existing >= 0) {
|
|
717
802
|
config.installedModules[existing] = record;
|
|
@@ -719,48 +804,125 @@ function recordModule(config, moduleId, manifest, now) {
|
|
|
719
804
|
config.installedModules.push(record);
|
|
720
805
|
}
|
|
721
806
|
}
|
|
807
|
+
async function pickModulesInteractively(localPath) {
|
|
808
|
+
const s = p5.spinner();
|
|
809
|
+
s.start("Loading available modules...");
|
|
810
|
+
let modules;
|
|
811
|
+
try {
|
|
812
|
+
modules = await listAvailableModules(localPath);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
s.stop("Failed to load modules.");
|
|
815
|
+
throw err;
|
|
816
|
+
}
|
|
817
|
+
s.stop("Modules loaded.");
|
|
818
|
+
if (modules.length === 0) {
|
|
819
|
+
p5.log.warn("No modules available.");
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
const options = [];
|
|
823
|
+
for (const mod of modules) {
|
|
824
|
+
options.push({
|
|
825
|
+
value: mod.name,
|
|
826
|
+
label: mod.name,
|
|
827
|
+
...mod.description !== void 0 ? { hint: mod.description } : {}
|
|
828
|
+
});
|
|
829
|
+
for (const sub of mod.subModules ?? []) {
|
|
830
|
+
options.push({
|
|
831
|
+
value: `${mod.name}/${sub}`,
|
|
832
|
+
label: ` \u21B3 ${sub}`,
|
|
833
|
+
hint: `sub-module of ${mod.name}`
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const selected = await p5.multiselect({
|
|
838
|
+
message: "Select modules to install:",
|
|
839
|
+
options,
|
|
840
|
+
required: true
|
|
841
|
+
});
|
|
842
|
+
if (p5.isCancel(selected)) {
|
|
843
|
+
p5.outro("Cancelled.");
|
|
844
|
+
process.exit(0);
|
|
845
|
+
}
|
|
846
|
+
const result = new Set(selected);
|
|
847
|
+
for (const id of selected) {
|
|
848
|
+
const { parent, child } = parseModuleId(id);
|
|
849
|
+
if (child !== null) {
|
|
850
|
+
result.add(parent);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return [...result];
|
|
854
|
+
}
|
|
722
855
|
async function runAdd(options) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
856
|
+
let { moduleNames, cwd, dryRun, localPath, withSubModules = [] } = options;
|
|
857
|
+
let allTargets;
|
|
858
|
+
if (moduleNames.length === 0) {
|
|
859
|
+
p5.intro(`impulse add${dryRun ? " [dry-run]" : ""}`);
|
|
860
|
+
await requireAuth();
|
|
861
|
+
if (withSubModules.length > 0) {
|
|
862
|
+
p5.log.warn(`--with is ignored in interactive mode. Select sub-modules from the picker.`);
|
|
863
|
+
withSubModules = [];
|
|
864
|
+
}
|
|
865
|
+
const picked = await pickModulesInteractively(localPath);
|
|
866
|
+
if (picked.length === 0) {
|
|
867
|
+
p5.outro("Nothing selected.");
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
allTargets = picked;
|
|
871
|
+
} else {
|
|
872
|
+
if (moduleNames.length > 1 && withSubModules.length > 0) {
|
|
873
|
+
p5.log.warn(
|
|
874
|
+
`--with is ignored when multiple modules are provided. Specify a single module to use --with.`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
const withIds = moduleNames.length === 1 && withSubModules.length > 0 ? withSubModules.map((sub) => `${moduleNames[0]}/${sub}`) : [];
|
|
878
|
+
allTargets = [...moduleNames, ...withIds];
|
|
879
|
+
p5.intro(`impulse add ${allTargets.join(", ")}${dryRun ? " [dry-run]" : ""}`);
|
|
880
|
+
await requireAuth();
|
|
881
|
+
}
|
|
727
882
|
const config = await readConfig(cwd);
|
|
728
883
|
if (!config) {
|
|
729
|
-
|
|
884
|
+
p5.cancel("No .impulse.json found. Run `impulse init` first.");
|
|
730
885
|
process.exit(1);
|
|
731
886
|
}
|
|
732
887
|
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
|
|
888
|
+
if (withSubModules.length === 0) {
|
|
889
|
+
const alreadyInstalled = allTargets.filter((id) => {
|
|
890
|
+
const { child } = parseModuleId(id);
|
|
891
|
+
return child === null && installedNames.has(id);
|
|
740
892
|
});
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
|
|
893
|
+
if (alreadyInstalled.length > 0) {
|
|
894
|
+
for (const id of alreadyInstalled) {
|
|
895
|
+
const existing = config.installedModules.find((m) => m.name === id);
|
|
896
|
+
p5.log.warn(`Module "${id}" is already installed (v${existing?.version ?? "?"}).`);
|
|
897
|
+
}
|
|
898
|
+
const reinstall = await p5.confirm({
|
|
899
|
+
message: alreadyInstalled.length === 1 ? "Reinstall?" : "Reinstall all?",
|
|
900
|
+
initialValue: false
|
|
901
|
+
});
|
|
902
|
+
if (p5.isCancel(reinstall) || !reinstall) {
|
|
903
|
+
p5.outro("Cancelled.");
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
744
906
|
}
|
|
745
907
|
}
|
|
746
|
-
if (withSubModules.length > 0) {
|
|
747
|
-
const parentManifest = await fetchModuleManifest(
|
|
908
|
+
if (moduleNames.length === 1 && withSubModules.length > 0) {
|
|
909
|
+
const parentManifest = await fetchModuleManifest(moduleNames[0], localPath).catch(() => null);
|
|
748
910
|
if (parentManifest) {
|
|
749
911
|
if (parentManifest.subModules.length === 0) {
|
|
750
|
-
|
|
912
|
+
p5.cancel(`"${moduleNames[0]}" has no declared sub-modules.`);
|
|
751
913
|
process.exit(1);
|
|
752
914
|
}
|
|
753
915
|
const invalid = withSubModules.filter((sub) => !parentManifest.subModules.includes(sub));
|
|
754
916
|
if (invalid.length > 0) {
|
|
755
|
-
|
|
756
|
-
`Unknown sub-module(s) for "${
|
|
917
|
+
p5.cancel(
|
|
918
|
+
`Unknown sub-module(s) for "${moduleNames[0]}": ${invalid.join(", ")}.
|
|
757
919
|
Available: ${parentManifest.subModules.join(", ")}`
|
|
758
920
|
);
|
|
759
921
|
process.exit(1);
|
|
760
922
|
}
|
|
761
923
|
}
|
|
762
924
|
}
|
|
763
|
-
const s =
|
|
925
|
+
const s = p5.spinner();
|
|
764
926
|
s.start("Resolving dependencies...");
|
|
765
927
|
const resolved = /* @__PURE__ */ new Set();
|
|
766
928
|
const orderedModules = [];
|
|
@@ -770,7 +932,7 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
770
932
|
}
|
|
771
933
|
} catch (err) {
|
|
772
934
|
s.stop("Dependency resolution failed.");
|
|
773
|
-
|
|
935
|
+
p5.cancel(err instanceof Error ? err.message : String(err));
|
|
774
936
|
process.exit(1);
|
|
775
937
|
}
|
|
776
938
|
s.stop(`Resolved: ${orderedModules.join(" \u2192 ")}`);
|
|
@@ -778,21 +940,34 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
778
940
|
for (const id of orderedModules) {
|
|
779
941
|
manifests.set(id, await fetchModuleManifest(id, localPath));
|
|
780
942
|
}
|
|
943
|
+
for (const [id, manifest] of manifests) {
|
|
944
|
+
if (installedNames.has(id)) continue;
|
|
945
|
+
for (const incompatible of manifest.incompatibleWith) {
|
|
946
|
+
if (installedNames.has(incompatible)) {
|
|
947
|
+
p5.cancel(`Module "${id}" is incompatible with installed module "${incompatible}".`);
|
|
948
|
+
process.exit(1);
|
|
949
|
+
}
|
|
950
|
+
if (manifests.has(incompatible) && !installedNames.has(incompatible)) {
|
|
951
|
+
p5.cancel(`Module "${id}" is incompatible with module "${incompatible}" (also being installed).`);
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
781
956
|
const allDeps = /* @__PURE__ */ new Set();
|
|
782
957
|
for (const manifest of manifests.values()) {
|
|
783
958
|
for (const dep of manifest.dependencies) {
|
|
784
959
|
allDeps.add(dep);
|
|
785
960
|
}
|
|
786
961
|
}
|
|
787
|
-
|
|
962
|
+
p5.log.message("\nSummary of changes:");
|
|
788
963
|
for (const [id, manifest] of manifests) {
|
|
789
|
-
|
|
964
|
+
p5.log.message(`
|
|
790
965
|
Module: ${id}@${manifest.version}`);
|
|
791
966
|
for (const file of manifest.files) {
|
|
792
|
-
|
|
967
|
+
p5.log.message(` + ${file.dest}`);
|
|
793
968
|
}
|
|
794
969
|
for (const transform of manifest.transforms) {
|
|
795
|
-
|
|
970
|
+
p5.log.message(` ~ ${transform.type} \u2192 ${transform.target}`);
|
|
796
971
|
}
|
|
797
972
|
}
|
|
798
973
|
const allEnvVars = /* @__PURE__ */ new Set();
|
|
@@ -802,33 +977,36 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
802
977
|
}
|
|
803
978
|
}
|
|
804
979
|
if (allDeps.size > 0) {
|
|
805
|
-
|
|
980
|
+
p5.log.message(`
|
|
806
981
|
npm deps: ${[...allDeps].join(", ")}`);
|
|
807
982
|
}
|
|
808
983
|
if (allEnvVars.size > 0) {
|
|
809
|
-
|
|
984
|
+
p5.log.message(`
|
|
810
985
|
env vars required: ${[...allEnvVars].join(", ")}`);
|
|
811
986
|
}
|
|
812
987
|
if (!dryRun) {
|
|
813
|
-
const
|
|
988
|
+
const confirm6 = await p5.confirm({
|
|
814
989
|
message: "Proceed?",
|
|
815
990
|
initialValue: true
|
|
816
991
|
});
|
|
817
|
-
if (
|
|
818
|
-
|
|
992
|
+
if (p5.isCancel(confirm6) || !confirm6) {
|
|
993
|
+
p5.outro("Cancelled.");
|
|
819
994
|
return;
|
|
820
995
|
}
|
|
821
996
|
}
|
|
822
997
|
const primaryTargetSet = new Set(allTargets);
|
|
823
998
|
const depModules = orderedModules.filter((id) => !primaryTargetSet.has(id) && !installedNames.has(id));
|
|
824
999
|
const targetModules = orderedModules.filter((id) => primaryTargetSet.has(id));
|
|
1000
|
+
const installedFilesMap = /* @__PURE__ */ new Map();
|
|
825
1001
|
const depPostInstallHooks = [];
|
|
826
1002
|
if (depModules.length > 0) {
|
|
827
|
-
|
|
1003
|
+
p5.log.step(`Installing module dependencies: ${depModules.join(", ")}`);
|
|
828
1004
|
for (const dep of depModules) {
|
|
829
1005
|
const depManifest = manifests.get(dep);
|
|
830
1006
|
if (!depManifest) continue;
|
|
831
|
-
await installModule(dep, depManifest, cwd, dryRun, localPath);
|
|
1007
|
+
const dests = await installModule(dep, depManifest, cwd, dryRun, installedNames, localPath);
|
|
1008
|
+
installedFilesMap.set(dep, dests);
|
|
1009
|
+
installedNames.add(dep);
|
|
832
1010
|
if (depManifest.postInstall && depManifest.postInstall.length > 0) {
|
|
833
1011
|
depPostInstallHooks.push({ name: dep, hooks: depManifest.postInstall });
|
|
834
1012
|
}
|
|
@@ -837,14 +1015,16 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
837
1015
|
for (const targetId of targetModules) {
|
|
838
1016
|
const targetManifest = manifests.get(targetId);
|
|
839
1017
|
if (!targetManifest) continue;
|
|
840
|
-
await installModule(targetId, targetManifest, cwd, dryRun, localPath);
|
|
1018
|
+
const dests = await installModule(targetId, targetManifest, cwd, dryRun, installedNames, localPath);
|
|
1019
|
+
installedFilesMap.set(targetId, dests);
|
|
1020
|
+
installedNames.add(targetId);
|
|
841
1021
|
}
|
|
842
1022
|
installNpmDeps([...allDeps], cwd, dryRun);
|
|
843
1023
|
if (!dryRun) {
|
|
844
1024
|
for (const { name, hooks } of depPostInstallHooks) {
|
|
845
|
-
|
|
1025
|
+
p5.log.step(`Running post-install hooks for ${name}...`);
|
|
846
1026
|
for (const hook of hooks) {
|
|
847
|
-
|
|
1027
|
+
p5.log.message(` $ ${hook}`);
|
|
848
1028
|
execSync2(hook, { cwd, stdio: "inherit" });
|
|
849
1029
|
}
|
|
850
1030
|
}
|
|
@@ -853,9 +1033,9 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
853
1033
|
for (const targetId of targetModules) {
|
|
854
1034
|
const targetManifest = manifests.get(targetId);
|
|
855
1035
|
if (!targetManifest?.postInstall?.length) continue;
|
|
856
|
-
|
|
1036
|
+
p5.log.step(`Running post-install hooks for ${targetId}...`);
|
|
857
1037
|
for (const hook of targetManifest.postInstall) {
|
|
858
|
-
|
|
1038
|
+
p5.log.message(` $ ${hook}`);
|
|
859
1039
|
execSync2(hook, { cwd, stdio: "inherit" });
|
|
860
1040
|
}
|
|
861
1041
|
}
|
|
@@ -864,72 +1044,328 @@ Available: ${parentManifest.subModules.join(", ")}`
|
|
|
864
1044
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
865
1045
|
for (const dep of depModules) {
|
|
866
1046
|
const depManifest = manifests.get(dep);
|
|
867
|
-
|
|
1047
|
+
const dests = installedFilesMap.get(dep) ?? [];
|
|
1048
|
+
if (depManifest) recordModule(config, dep, depManifest, dests, now);
|
|
868
1049
|
}
|
|
869
1050
|
for (const targetId of targetModules) {
|
|
870
1051
|
const targetManifest = manifests.get(targetId);
|
|
871
|
-
|
|
1052
|
+
const dests = installedFilesMap.get(targetId) ?? [];
|
|
1053
|
+
if (targetManifest) recordModule(config, targetId, targetManifest, dests, now);
|
|
872
1054
|
}
|
|
873
1055
|
await writeConfig(config, cwd);
|
|
874
1056
|
}
|
|
875
1057
|
const label = allTargets.length === 1 ? `"${allTargets[0]}"` : allTargets.map((t) => `"${t}"`).join(", ");
|
|
876
|
-
|
|
1058
|
+
p5.outro(
|
|
877
1059
|
dryRun ? "Dry run complete \u2014 no files were modified." : `Module(s) ${label} installed successfully!`
|
|
878
1060
|
);
|
|
879
1061
|
}
|
|
880
1062
|
|
|
881
1063
|
// src/commands/list.ts
|
|
882
|
-
import * as
|
|
1064
|
+
import * as p6 from "@clack/prompts";
|
|
1065
|
+
function printModule(mod, installedNames, installedModules) {
|
|
1066
|
+
const installed = installedNames.has(mod.name);
|
|
1067
|
+
const installedInfo = installed ? installedModules?.find((m) => m.name === mod.name) : null;
|
|
1068
|
+
const status = installed ? `[installed v${installedInfo?.version ?? "?"}]` : "[available]";
|
|
1069
|
+
const desc = mod.description ? ` \u2014 ${mod.description}` : "";
|
|
1070
|
+
p6.log.message(` ${installed ? "\u2713" : "\u25CB"} ${mod.name} ${status}${desc}`);
|
|
1071
|
+
if (mod.subModules && mod.subModules.length > 0) {
|
|
1072
|
+
const last = mod.subModules.length - 1;
|
|
1073
|
+
mod.subModules.forEach((sub, i) => {
|
|
1074
|
+
const subId = `${mod.name}/${sub}`;
|
|
1075
|
+
const subInstalled = installedNames.has(subId);
|
|
1076
|
+
const subInfo = subInstalled ? installedModules?.find((m) => m.name === subId) : null;
|
|
1077
|
+
const subStatus = subInstalled ? `[installed v${subInfo?.version ?? "?"}]` : "[not installed]";
|
|
1078
|
+
const connector = i === last ? "\u2514\u2500" : "\u251C\u2500";
|
|
1079
|
+
p6.log.message(` ${connector} ${sub} ${subStatus}`);
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
883
1083
|
async function runList(options) {
|
|
884
1084
|
const { cwd, localPath } = options;
|
|
885
|
-
|
|
1085
|
+
p6.intro("impulse list");
|
|
1086
|
+
await requireAuth();
|
|
886
1087
|
const config = await readConfig(cwd);
|
|
887
1088
|
const installedNames = new Set(
|
|
888
1089
|
config?.installedModules.map((m) => m.name) ?? []
|
|
889
1090
|
);
|
|
890
|
-
const s =
|
|
1091
|
+
const s = p6.spinner();
|
|
891
1092
|
s.start("Fetching available modules...");
|
|
892
1093
|
let available;
|
|
893
1094
|
try {
|
|
894
1095
|
available = await listAvailableModules(localPath);
|
|
895
1096
|
} catch (err) {
|
|
896
1097
|
s.stop("Failed to fetch module list.");
|
|
897
|
-
|
|
1098
|
+
p6.cancel(err instanceof Error ? err.message : String(err));
|
|
898
1099
|
process.exit(1);
|
|
899
1100
|
}
|
|
900
1101
|
s.stop(`Found ${available.length} module(s).`);
|
|
901
1102
|
if (available.length === 0) {
|
|
902
|
-
|
|
903
|
-
|
|
1103
|
+
p6.log.message("No modules available yet.");
|
|
1104
|
+
p6.outro("Done.");
|
|
904
1105
|
return;
|
|
905
1106
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1107
|
+
p6.log.message("\nAvailable modules:\n");
|
|
1108
|
+
const hasCategories = available.some((m) => m.category !== void 0);
|
|
1109
|
+
if (hasCategories) {
|
|
1110
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1111
|
+
for (const mod of available) {
|
|
1112
|
+
const key = mod.category ?? "other";
|
|
1113
|
+
const existing = groups.get(key);
|
|
1114
|
+
if (existing) {
|
|
1115
|
+
existing.push(mod);
|
|
1116
|
+
} else {
|
|
1117
|
+
groups.set(key, [mod]);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const categoryOrder = ["core", "feature", "integration", "dx", "other"];
|
|
1121
|
+
const sortedKeys = [
|
|
1122
|
+
...categoryOrder.filter((k) => groups.has(k)),
|
|
1123
|
+
...[...groups.keys()].filter((k) => !categoryOrder.includes(k))
|
|
1124
|
+
];
|
|
1125
|
+
for (const key of sortedKeys) {
|
|
1126
|
+
p6.log.message(`${key.toUpperCase()}`);
|
|
1127
|
+
for (const mod of groups.get(key)) {
|
|
1128
|
+
printModule(mod, installedNames, config?.installedModules);
|
|
1129
|
+
}
|
|
1130
|
+
p6.log.message("");
|
|
1131
|
+
}
|
|
1132
|
+
} else {
|
|
1133
|
+
for (const mod of available) {
|
|
1134
|
+
printModule(mod, installedNames, config?.installedModules);
|
|
923
1135
|
}
|
|
924
1136
|
}
|
|
925
|
-
|
|
1137
|
+
p6.log.message(
|
|
926
1138
|
`
|
|
927
1139
|
Run \`impulse add <module>\` to install a module.`
|
|
928
1140
|
);
|
|
929
|
-
|
|
1141
|
+
p6.log.message(
|
|
930
1142
|
`Run \`impulse add <parent>/<sub>\` or \`impulse add <parent> --with <sub1>,<sub2>\` for sub-modules.`
|
|
931
1143
|
);
|
|
932
|
-
|
|
1144
|
+
p6.outro("Done.");
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// src/commands/login.ts
|
|
1148
|
+
import * as p7 from "@clack/prompts";
|
|
1149
|
+
|
|
1150
|
+
// src/auth/device-flow.ts
|
|
1151
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1152
|
+
var IMPULSE_BASE_URL = process.env.IMPULSE_BASE_URL ?? "https://impulse.studio";
|
|
1153
|
+
async function requestDeviceCode() {
|
|
1154
|
+
const res = await fetch(`${IMPULSE_BASE_URL}/api/auth/device/code`, {
|
|
1155
|
+
method: "POST",
|
|
1156
|
+
headers: { "Content-Type": "application/json" },
|
|
1157
|
+
body: JSON.stringify({ client: "impulse-cli" })
|
|
1158
|
+
});
|
|
1159
|
+
if (!res.ok) {
|
|
1160
|
+
throw new Error(`Failed to initiate device flow: ${res.status} ${res.statusText}`);
|
|
1161
|
+
}
|
|
1162
|
+
const data = await res.json();
|
|
1163
|
+
assertDeviceCodeResponse(data);
|
|
1164
|
+
return data;
|
|
1165
|
+
}
|
|
1166
|
+
async function pollDeviceToken(deviceCode) {
|
|
1167
|
+
let res;
|
|
1168
|
+
try {
|
|
1169
|
+
res = await fetch(`${IMPULSE_BASE_URL}/api/auth/device/token`, {
|
|
1170
|
+
method: "POST",
|
|
1171
|
+
headers: { "Content-Type": "application/json" },
|
|
1172
|
+
body: JSON.stringify({ deviceCode })
|
|
1173
|
+
});
|
|
1174
|
+
} catch {
|
|
1175
|
+
return { status: "error", message: "Network error while polling for token" };
|
|
1176
|
+
}
|
|
1177
|
+
if (res.status === 200) {
|
|
1178
|
+
const data = await res.json();
|
|
1179
|
+
assertDeviceTokenResponse(data);
|
|
1180
|
+
return { status: "authorized", data };
|
|
1181
|
+
}
|
|
1182
|
+
if (res.status === 202) {
|
|
1183
|
+
return { status: "pending" };
|
|
1184
|
+
}
|
|
1185
|
+
if (res.status === 400) {
|
|
1186
|
+
let body;
|
|
1187
|
+
try {
|
|
1188
|
+
body = await res.json();
|
|
1189
|
+
} catch {
|
|
1190
|
+
body = {};
|
|
1191
|
+
}
|
|
1192
|
+
const error = typeof body === "object" && body !== null && "error" in body ? String(body.error) : "";
|
|
1193
|
+
if (error === "slow_down") return { status: "slow_down" };
|
|
1194
|
+
if (error === "expired" || error === "authorization_expired") return { status: "expired" };
|
|
1195
|
+
return { status: "error", message: error || "Device code error" };
|
|
1196
|
+
}
|
|
1197
|
+
return { status: "error", message: `Unexpected response: ${res.status}` };
|
|
1198
|
+
}
|
|
1199
|
+
function openBrowser(url) {
|
|
1200
|
+
try {
|
|
1201
|
+
const platform = process.platform;
|
|
1202
|
+
if (platform === "darwin") {
|
|
1203
|
+
execFileSync2("open", [url]);
|
|
1204
|
+
} else if (platform === "win32") {
|
|
1205
|
+
execFileSync2("cmd", ["/c", "start", url]);
|
|
1206
|
+
} else {
|
|
1207
|
+
execFileSync2("xdg-open", [url]);
|
|
1208
|
+
}
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function assertDeviceCodeResponse(data) {
|
|
1213
|
+
if (typeof data !== "object" || data === null || typeof data.deviceCode !== "string" || typeof data.userCode !== "string" || typeof data.verificationUri !== "string") {
|
|
1214
|
+
throw new Error("Invalid device code response from server");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function assertDeviceTokenResponse(data) {
|
|
1218
|
+
if (typeof data !== "object" || data === null || typeof data.token !== "string" || typeof data.email !== "string") {
|
|
1219
|
+
throw new Error("Invalid token response from server");
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/auth/write-auth.ts
|
|
1224
|
+
import fsExtra14 from "fs-extra";
|
|
1225
|
+
import { chmod } from "fs/promises";
|
|
1226
|
+
var { outputJson } = fsExtra14;
|
|
1227
|
+
async function writeAuth(credentials) {
|
|
1228
|
+
const file = authPath();
|
|
1229
|
+
await outputJson(file, credentials, { spaces: 2, mode: 384 });
|
|
1230
|
+
await chmod(file, 384);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/commands/login.ts
|
|
1234
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1235
|
+
var MAX_POLL_DURATION_MS = 5 * 60 * 1e3;
|
|
1236
|
+
async function runLogin() {
|
|
1237
|
+
p7.intro("impulse login");
|
|
1238
|
+
const existing = await readAuth();
|
|
1239
|
+
if (existing) {
|
|
1240
|
+
const reauth = await p7.confirm({
|
|
1241
|
+
message: `Already logged in as ${existing.email}. Log in again?`,
|
|
1242
|
+
initialValue: false
|
|
1243
|
+
});
|
|
1244
|
+
if (p7.isCancel(reauth) || !reauth) {
|
|
1245
|
+
p7.outro("Cancelled.");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const s = p7.spinner();
|
|
1250
|
+
s.start("Initiating login...");
|
|
1251
|
+
let deviceCode;
|
|
1252
|
+
try {
|
|
1253
|
+
deviceCode = await requestDeviceCode();
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
s.stop("Failed to initiate login.");
|
|
1256
|
+
p7.cancel(err instanceof Error ? err.message : String(err));
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
s.stop("Opening browser for authentication...");
|
|
1260
|
+
const browserUrl = deviceCode.verificationUri;
|
|
1261
|
+
p7.log.message(`
|
|
1262
|
+
Visit the following URL to authenticate:
|
|
1263
|
+
${browserUrl}
|
|
1264
|
+
`);
|
|
1265
|
+
p7.log.message(`Your one-time code: ${deviceCode.userCode}
|
|
1266
|
+
`);
|
|
1267
|
+
openBrowser(browserUrl);
|
|
1268
|
+
p7.log.info(`If the browser did not open, copy the URL above.
|
|
1269
|
+
Base URL: ${IMPULSE_BASE_URL}`);
|
|
1270
|
+
const pollInterval = Math.max(
|
|
1271
|
+
(deviceCode.interval ?? 5) * 1e3,
|
|
1272
|
+
DEFAULT_POLL_INTERVAL_MS
|
|
1273
|
+
);
|
|
1274
|
+
const expiresAt = Date.now() + (deviceCode.expiresIn ?? 300) * 1e3;
|
|
1275
|
+
const deadline = Math.min(expiresAt, Date.now() + MAX_POLL_DURATION_MS);
|
|
1276
|
+
const pollSpinner = p7.spinner();
|
|
1277
|
+
pollSpinner.start("Waiting for authentication...");
|
|
1278
|
+
let currentInterval = pollInterval;
|
|
1279
|
+
while (Date.now() < deadline) {
|
|
1280
|
+
await sleep(currentInterval);
|
|
1281
|
+
const result = await pollDeviceToken(deviceCode.deviceCode);
|
|
1282
|
+
if (result.status === "authorized") {
|
|
1283
|
+
pollSpinner.stop("Authentication successful!");
|
|
1284
|
+
await writeAuth({
|
|
1285
|
+
token: result.data.token,
|
|
1286
|
+
refreshToken: result.data.refreshToken,
|
|
1287
|
+
expiresAt: result.data.expiresAt,
|
|
1288
|
+
email: result.data.email
|
|
1289
|
+
});
|
|
1290
|
+
p7.outro(`Logged in as ${result.data.email}`);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (result.status === "slow_down") {
|
|
1294
|
+
currentInterval += 5e3;
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (result.status === "expired") {
|
|
1298
|
+
pollSpinner.stop("Device code expired.");
|
|
1299
|
+
p7.cancel("Authentication timed out. Run `impulse login` again.");
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
}
|
|
1302
|
+
if (result.status === "error") {
|
|
1303
|
+
pollSpinner.stop("Authentication failed.");
|
|
1304
|
+
p7.cancel(result.message);
|
|
1305
|
+
process.exit(1);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
pollSpinner.stop("Authentication timed out.");
|
|
1309
|
+
p7.cancel("Authentication timed out. Run `impulse login` again.");
|
|
1310
|
+
process.exit(1);
|
|
1311
|
+
}
|
|
1312
|
+
function sleep(ms) {
|
|
1313
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// src/commands/logout.ts
|
|
1317
|
+
import * as p8 from "@clack/prompts";
|
|
1318
|
+
|
|
1319
|
+
// src/auth/clear-auth.ts
|
|
1320
|
+
import fsExtra15 from "fs-extra";
|
|
1321
|
+
var { remove, pathExists: pathExists13 } = fsExtra15;
|
|
1322
|
+
async function clearAuth() {
|
|
1323
|
+
const file = authPath();
|
|
1324
|
+
if (!await pathExists13(file)) return false;
|
|
1325
|
+
await remove(file);
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/commands/logout.ts
|
|
1330
|
+
async function runLogout() {
|
|
1331
|
+
p8.intro("impulse logout");
|
|
1332
|
+
const credentials = await readAuth();
|
|
1333
|
+
if (!credentials) {
|
|
1334
|
+
p8.outro("Not currently logged in.");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const confirm6 = await p8.confirm({
|
|
1338
|
+
message: `Log out ${credentials.email}?`,
|
|
1339
|
+
initialValue: true
|
|
1340
|
+
});
|
|
1341
|
+
if (p8.isCancel(confirm6) || !confirm6) {
|
|
1342
|
+
p8.outro("Cancelled.");
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
await clearAuth();
|
|
1346
|
+
p8.outro("Logged out successfully.");
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/commands/whoami.ts
|
|
1350
|
+
import * as p9 from "@clack/prompts";
|
|
1351
|
+
async function runWhoami() {
|
|
1352
|
+
p9.intro("impulse whoami");
|
|
1353
|
+
const credentials = await readAuth();
|
|
1354
|
+
if (!credentials) {
|
|
1355
|
+
p9.cancel("Not authenticated. Run `impulse login` first.");
|
|
1356
|
+
process.exit(1);
|
|
1357
|
+
}
|
|
1358
|
+
p9.log.message(`Logged in as: ${credentials.email}`);
|
|
1359
|
+
if (credentials.expiresAt) {
|
|
1360
|
+
const expires = new Date(credentials.expiresAt);
|
|
1361
|
+
const now = /* @__PURE__ */ new Date();
|
|
1362
|
+
if (expires < now) {
|
|
1363
|
+
p9.log.warn("Your session has expired. Run `impulse login` to re-authenticate.");
|
|
1364
|
+
} else {
|
|
1365
|
+
p9.log.message(`Session expires: ${expires.toLocaleString()}`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
p9.outro("Done.");
|
|
933
1369
|
}
|
|
934
1370
|
|
|
935
1371
|
// src/cli-version.ts
|
|
@@ -943,17 +1379,17 @@ program.name("impulse").description("ImpulseLab CLI \u2014 install and manage mo
|
|
|
943
1379
|
program.command("init").description("Initialize impulse in the current project").option("--force", "Reinitialize even if .impulse.json already exists", false).action(async (options) => {
|
|
944
1380
|
await runInit({ cwd: process.cwd(), force: options.force });
|
|
945
1381
|
});
|
|
946
|
-
program.command("add
|
|
947
|
-
"Add
|
|
1382
|
+
program.command("add [modules...]").description(
|
|
1383
|
+
"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`"
|
|
948
1384
|
).option("--dry-run", "Preview changes without writing files", false).option(
|
|
949
1385
|
"--local <path>",
|
|
950
1386
|
"Use a local modules directory (for development)"
|
|
951
1387
|
).option(
|
|
952
1388
|
"--with <submodules>",
|
|
953
1389
|
"Comma-separated sub-modules to install alongside the parent (e.g. --with quote-to-cash,gocardless)"
|
|
954
|
-
).action(async (
|
|
1390
|
+
).action(async (modules, options) => {
|
|
955
1391
|
const addOpts = {
|
|
956
|
-
|
|
1392
|
+
moduleNames: modules ?? [],
|
|
957
1393
|
cwd: process.cwd(),
|
|
958
1394
|
dryRun: options.dryRun
|
|
959
1395
|
};
|
|
@@ -971,4 +1407,13 @@ program.command("list").description("List available and installed modules").opti
|
|
|
971
1407
|
if (options.local !== void 0) listOpts.localPath = options.local;
|
|
972
1408
|
await runList(listOpts);
|
|
973
1409
|
});
|
|
1410
|
+
program.command("login").description("Authenticate with ImpulseLab (opens browser for device flow)").action(async () => {
|
|
1411
|
+
await runLogin();
|
|
1412
|
+
});
|
|
1413
|
+
program.command("logout").description("Log out and clear stored credentials").action(async () => {
|
|
1414
|
+
await runLogout();
|
|
1415
|
+
});
|
|
1416
|
+
program.command("whoami").description("Show the currently authenticated user").action(async () => {
|
|
1417
|
+
await runWhoami();
|
|
1418
|
+
});
|
|
974
1419
|
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.4",
|
|
4
4
|
"description": "ImpulseLab CLI — install and manage modules for your projects",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"dev": "tsup src/index.ts --format esm --target es2022 --watch",
|
|
40
40
|
"lint": "oxlint .",
|
|
41
41
|
"test": "vitest run",
|
|
42
|
+
"release:publish": "pnpm run test && pnpm run build && pnpm publish --no-git-checks --access public",
|
|
42
43
|
"release": "bumpp"
|
|
43
44
|
}
|
|
44
45
|
}
|