@sundaeswap/sprinkles 0.6.1 → 0.7.0
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/cjs/Sprinkle/__tests__/action-integration.test.js +590 -0
- package/dist/cjs/Sprinkle/__tests__/action-integration.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/action-registry.test.js +193 -0
- package/dist/cjs/Sprinkle/__tests__/action-registry.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/action-runner.test.js +304 -0
- package/dist/cjs/Sprinkle/__tests__/action-runner.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +1110 -0
- package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +722 -0
- package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +713 -0
- package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
- package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
- package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
- package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/index.js +117 -0
- package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
- package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
- package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
- package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
- package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/cli-adapter.js +372 -0
- package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/index.js +127 -0
- package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/mcp-adapter.js +415 -0
- package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/registry.js +92 -0
- package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/runner.js +190 -0
- package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
- package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/types.js +68 -0
- package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
- package/dist/cjs/Sprinkle/index.js +412 -1
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/prompts.js +12 -7
- package/dist/cjs/Sprinkle/prompts.js.map +1 -1
- package/dist/cjs/Sprinkle/type-guards.js +7 -1
- package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
- package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
- package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
- package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
- package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +720 -0
- package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +712 -0
- package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
- package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
- package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
- package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/index.js +32 -0
- package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
- package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
- package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
- package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
- package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/cli-adapter.js +361 -0
- package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
- package/dist/esm/Sprinkle/actions/index.js +12 -0
- package/dist/esm/Sprinkle/actions/index.js.map +1 -0
- package/dist/esm/Sprinkle/actions/mcp-adapter.js +407 -0
- package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
- package/dist/esm/Sprinkle/actions/registry.js +85 -0
- package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
- package/dist/esm/Sprinkle/actions/runner.js +182 -0
- package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
- package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
- package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
- package/dist/esm/Sprinkle/actions/types.js +61 -0
- package/dist/esm/Sprinkle/actions/types.js.map +1 -0
- package/dist/esm/Sprinkle/index.js +260 -1
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/prompts.js +12 -7
- package/dist/esm/Sprinkle/prompts.js.map +1 -1
- package/dist/esm/Sprinkle/type-guards.js +3 -0
- package/dist/esm/Sprinkle/type-guards.js.map +1 -1
- package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
- package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/index.d.ts +26 -0
- package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
- package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
- package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
- package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
- package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
- package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/index.d.ts +12 -0
- package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +92 -0
- package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
- package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
- package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
- package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/types.d.ts +76 -0
- package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
- package/dist/types/Sprinkle/index.d.ts +81 -1
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
- package/dist/types/Sprinkle/type-guards.d.ts +4 -1
- package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +9 -2
- package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
- package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
- package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
- package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
- package/src/Sprinkle/__tests__/cli-adapter.test.ts +715 -0
- package/src/Sprinkle/__tests__/mcp-adapter.test.ts +718 -0
- package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
- package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
- package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
- package/src/Sprinkle/actions/builtin/index.ts +86 -0
- package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
- package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
- package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
- package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
- package/src/Sprinkle/actions/cli-adapter.ts +430 -0
- package/src/Sprinkle/actions/index.ts +32 -0
- package/src/Sprinkle/actions/mcp-adapter.ts +463 -0
- package/src/Sprinkle/actions/registry.ts +97 -0
- package/src/Sprinkle/actions/runner.ts +200 -0
- package/src/Sprinkle/actions/tui-helpers.ts +114 -0
- package/src/Sprinkle/actions/types.ts +91 -0
- package/src/Sprinkle/index.ts +351 -0
- package/src/Sprinkle/prompts.ts +118 -72
- package/src/Sprinkle/type-guards.ts +9 -0
package/src/Sprinkle/index.ts
CHANGED
|
@@ -69,7 +69,10 @@ import {
|
|
|
69
69
|
isImport,
|
|
70
70
|
isArray,
|
|
71
71
|
isBigInt,
|
|
72
|
+
isBoolean,
|
|
73
|
+
isInteger,
|
|
72
74
|
isLiteral,
|
|
75
|
+
isNumber,
|
|
73
76
|
isObject,
|
|
74
77
|
isRef,
|
|
75
78
|
isString,
|
|
@@ -88,7 +91,10 @@ export {
|
|
|
88
91
|
isImport,
|
|
89
92
|
isArray,
|
|
90
93
|
isBigInt,
|
|
94
|
+
isBoolean,
|
|
95
|
+
isInteger,
|
|
91
96
|
isLiteral,
|
|
97
|
+
isNumber,
|
|
92
98
|
isObject,
|
|
93
99
|
isRef,
|
|
94
100
|
isString,
|
|
@@ -146,6 +152,47 @@ import {
|
|
|
146
152
|
import { promptObject, promptArray } from "./menus/index.js";
|
|
147
153
|
import { formatPath } from "./utils/formatting.js";
|
|
148
154
|
|
|
155
|
+
// Import and re-export action system
|
|
156
|
+
import {
|
|
157
|
+
ActionRegistry,
|
|
158
|
+
ActionError,
|
|
159
|
+
executeAction,
|
|
160
|
+
detectMode,
|
|
161
|
+
parseCliArgs,
|
|
162
|
+
generateActionHelp,
|
|
163
|
+
camelToKebab,
|
|
164
|
+
kebabToCamel,
|
|
165
|
+
coerceValue,
|
|
166
|
+
parseArgvWithSchema,
|
|
167
|
+
generateAppHelp,
|
|
168
|
+
runCli,
|
|
169
|
+
runMcp,
|
|
170
|
+
promptAndExecute,
|
|
171
|
+
} from "./actions/index.js";
|
|
172
|
+
import type { AnyAction, IActionContext, IActionResult } from "./actions/index.js";
|
|
173
|
+
export type { IAction, IActionContext, IActionResult, IActionSuccess, IActionFailure, AnyAction } from "./actions/index.js";
|
|
174
|
+
export {
|
|
175
|
+
ActionRegistry,
|
|
176
|
+
ActionError,
|
|
177
|
+
executeAction,
|
|
178
|
+
detectMode,
|
|
179
|
+
parseCliArgs,
|
|
180
|
+
generateActionHelp,
|
|
181
|
+
camelToKebab,
|
|
182
|
+
kebabToCamel,
|
|
183
|
+
coerceValue,
|
|
184
|
+
parseArgvWithSchema,
|
|
185
|
+
generateAppHelp,
|
|
186
|
+
runCli,
|
|
187
|
+
typeboxToJsonSchema,
|
|
188
|
+
coerceMcpInput,
|
|
189
|
+
getMcpSdk,
|
|
190
|
+
createMcpServer,
|
|
191
|
+
runMcp,
|
|
192
|
+
getBuiltinActions,
|
|
193
|
+
promptAndExecute,
|
|
194
|
+
} from "./actions/index.js";
|
|
195
|
+
|
|
149
196
|
export interface IMenuAction<S extends TSchema> {
|
|
150
197
|
title: string;
|
|
151
198
|
action: (sprinkle: Sprinkle<S>) => Promise<Sprinkle<S> | void>;
|
|
@@ -165,11 +212,13 @@ export class Sprinkle<S extends TSchema> {
|
|
|
165
212
|
options: ISprinkleOptions;
|
|
166
213
|
profileId: string = "";
|
|
167
214
|
profileMeta: IProfileMeta = { name: "", createdAt: "", updatedAt: "" };
|
|
215
|
+
actionRegistry: ActionRegistry<S>;
|
|
168
216
|
|
|
169
217
|
constructor(type: S, storagePath: string, options?: ISprinkleOptions) {
|
|
170
218
|
this.type = type;
|
|
171
219
|
this.storagePath = storagePath;
|
|
172
220
|
this.options = options ?? {};
|
|
221
|
+
this.actionRegistry = new ActionRegistry<S>();
|
|
173
222
|
}
|
|
174
223
|
|
|
175
224
|
// --- Current Profile Accessor ---
|
|
@@ -771,6 +820,104 @@ export class Sprinkle<S extends TSchema> {
|
|
|
771
820
|
return maskSensitiveFields(this.settings, this.type);
|
|
772
821
|
}
|
|
773
822
|
|
|
823
|
+
// --- Non-interactive profile management (for CLI/MCP actions) ---
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Look up a profile entry by its ID without loading it.
|
|
827
|
+
* Returns undefined if no profile with the given ID exists.
|
|
828
|
+
*/
|
|
829
|
+
getProfileById(id: string): IProfileEntry | undefined {
|
|
830
|
+
return this.scanProfiles().find((p) => p.id === id);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Create a new profile file without interactive prompts.
|
|
835
|
+
* Does NOT switch the active profile.
|
|
836
|
+
*
|
|
837
|
+
* @throws ActionError with code DUPLICATE_PROFILE if a profile with the same name already exists.
|
|
838
|
+
* @returns The created IProfileEntry
|
|
839
|
+
*/
|
|
840
|
+
async createProfileNonInteractive(
|
|
841
|
+
name: string,
|
|
842
|
+
description?: string,
|
|
843
|
+
initialSettings?: Partial<TExact<S>>,
|
|
844
|
+
): Promise<IProfileEntry> {
|
|
845
|
+
const profiles = this.scanProfiles();
|
|
846
|
+
const nameLower = name.toLowerCase();
|
|
847
|
+
const duplicate = profiles.find(
|
|
848
|
+
(p) => p.meta.name.toLowerCase() === nameLower,
|
|
849
|
+
);
|
|
850
|
+
if (duplicate) {
|
|
851
|
+
throw new ActionError(
|
|
852
|
+
`A profile named "${name}" already exists.`,
|
|
853
|
+
"DUPLICATE_PROFILE",
|
|
854
|
+
{ existingId: duplicate.id },
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const profilesDir = Sprinkle.profilesDir(this.storagePath);
|
|
859
|
+
if (!fs.existsSync(profilesDir)) {
|
|
860
|
+
fs.mkdirSync(profilesDir, { recursive: true });
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const baseId = Sprinkle.sanitizeProfileId(name);
|
|
864
|
+
const id = Sprinkle.findAvailableId(profilesDir, baseId);
|
|
865
|
+
const now = new Date().toISOString();
|
|
866
|
+
const meta: IProfileMeta = { name, description, createdAt: now, updatedAt: now };
|
|
867
|
+
|
|
868
|
+
const profileData = {
|
|
869
|
+
meta,
|
|
870
|
+
settings: initialSettings ?? {},
|
|
871
|
+
defaults: {},
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
fs.writeFileSync(
|
|
875
|
+
path.join(profilesDir, `${id}.json`),
|
|
876
|
+
JSON.stringify(profileData, bigIntReplacer, 2),
|
|
877
|
+
"utf-8",
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
return { id, meta };
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Delete a profile file by ID.
|
|
885
|
+
*
|
|
886
|
+
* @throws ActionError with code PROFILE_NOT_FOUND if no profile with the given ID exists.
|
|
887
|
+
* @throws ActionError with code CANNOT_DELETE_ONLY_PROFILE if this is the only profile.
|
|
888
|
+
* @throws ActionError with code CANNOT_DELETE_ACTIVE_PROFILE if this is the currently active profile.
|
|
889
|
+
*/
|
|
890
|
+
deleteProfileById(id: string): void {
|
|
891
|
+
const profiles = this.scanProfiles();
|
|
892
|
+
const profile = profiles.find((p) => p.id === id);
|
|
893
|
+
|
|
894
|
+
if (!profile) {
|
|
895
|
+
throw new ActionError(
|
|
896
|
+
`Profile "${id}" not found.`,
|
|
897
|
+
"PROFILE_NOT_FOUND",
|
|
898
|
+
{ id },
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (profiles.length === 1) {
|
|
903
|
+
throw new ActionError(
|
|
904
|
+
"Cannot delete the only profile.",
|
|
905
|
+
"CANNOT_DELETE_ONLY_PROFILE",
|
|
906
|
+
{ id },
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (id === this.profileId) {
|
|
911
|
+
throw new ActionError(
|
|
912
|
+
"Cannot delete the active profile. Switch to a different profile first.",
|
|
913
|
+
"CANNOT_DELETE_ACTIVE_PROFILE",
|
|
914
|
+
{ id },
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
fs.unlinkSync(Sprinkle.profilePath(this.storagePath, id));
|
|
919
|
+
}
|
|
920
|
+
|
|
774
921
|
async TxDialog<P extends Provider, W extends Wallet>(
|
|
775
922
|
blaze: Blaze<P, W>,
|
|
776
923
|
tx: Core.Transaction,
|
|
@@ -1376,4 +1523,208 @@ export class Sprinkle<S extends TSchema> {
|
|
|
1376
1523
|
|
|
1377
1524
|
return def;
|
|
1378
1525
|
}
|
|
1526
|
+
|
|
1527
|
+
// --- Action System ---
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Register an action on this Sprinkle instance.
|
|
1531
|
+
* Delegates to the internal ActionRegistry with name and schema validation.
|
|
1532
|
+
*/
|
|
1533
|
+
registerAction(action: AnyAction<S>): void {
|
|
1534
|
+
this.actionRegistry.register(action);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* Retrieve a registered action by name.
|
|
1539
|
+
* @returns The action, or undefined if not registered
|
|
1540
|
+
*/
|
|
1541
|
+
getAction(name: string): AnyAction<S> | undefined {
|
|
1542
|
+
return this.actionRegistry.get(name);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* List all registered actions.
|
|
1547
|
+
*/
|
|
1548
|
+
listActions(): AnyAction<S>[] {
|
|
1549
|
+
return this.actionRegistry.list();
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* List all registered actions grouped by category.
|
|
1554
|
+
* Uncategorized actions are grouped under "default".
|
|
1555
|
+
*/
|
|
1556
|
+
listActionsByCategory(): Map<string, AnyAction<S>[]> {
|
|
1557
|
+
return this.actionRegistry.listByCategory();
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Execute a registered action by name with raw (unvalidated) input.
|
|
1562
|
+
* Input is validated against the action's inputSchema before execution.
|
|
1563
|
+
*
|
|
1564
|
+
* @throws Error if the action name is not registered
|
|
1565
|
+
*/
|
|
1566
|
+
async runAction(
|
|
1567
|
+
name: string,
|
|
1568
|
+
input: unknown,
|
|
1569
|
+
): Promise<IActionResult<unknown>> {
|
|
1570
|
+
const action = this.actionRegistry.get(name);
|
|
1571
|
+
if (!action) {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
`Action "${name}" is not registered. Available actions: ${this.actionRegistry.list().map((a) => a.name).join(", ") || "(none)"}`,
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const context: IActionContext<S> = {
|
|
1578
|
+
sprinkle: this,
|
|
1579
|
+
settings: this.settings,
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
return executeAction(action, input, context);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Non-interactive profile initialization for CLI/MCP modes.
|
|
1587
|
+
* Resolves a profile without any user prompts.
|
|
1588
|
+
*
|
|
1589
|
+
* Resolution order:
|
|
1590
|
+
* 1. If profileName provided, load that profile directly
|
|
1591
|
+
* 2. If exactly one profile exists, auto-select it
|
|
1592
|
+
* 3. If multiple profiles exist and no name given, throw with available names
|
|
1593
|
+
* 4. If no profiles exist, throw instructing user to run in interactive mode
|
|
1594
|
+
*/
|
|
1595
|
+
private async initNonInteractive(profileName?: string): Promise<void> {
|
|
1596
|
+
await this.migrateIfNeeded();
|
|
1597
|
+
|
|
1598
|
+
if (profileName) {
|
|
1599
|
+
await this.loadProfile(profileName);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const profiles = this.scanProfiles();
|
|
1604
|
+
|
|
1605
|
+
if (profiles.length === 0) {
|
|
1606
|
+
throw new Error(
|
|
1607
|
+
"No profiles found. Run in interactive mode to create one.",
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (profiles.length === 1) {
|
|
1612
|
+
await this.loadProfile(profiles[0]!.id);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Multiple profiles without a --profile flag
|
|
1617
|
+
const names = profiles.map((p) => `"${p.id}" (${p.meta.name})`).join(", ");
|
|
1618
|
+
throw new Error(
|
|
1619
|
+
`Multiple profiles found. Specify one with --profile <id>. Available profiles: ${names}`,
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* Static entry point that detects mode and runs accordingly.
|
|
1625
|
+
*
|
|
1626
|
+
* Modes:
|
|
1627
|
+
* - "tui" -- Interactive TUI menu (requires menu option)
|
|
1628
|
+
* - "cli" -- CLI action execution (parses argv for action name and args)
|
|
1629
|
+
* - "mcp" -- MCP protocol mode (not yet implemented)
|
|
1630
|
+
* - "help" -- Print available actions and exit
|
|
1631
|
+
*/
|
|
1632
|
+
static async run<S extends TSchema>(opts: {
|
|
1633
|
+
type: S;
|
|
1634
|
+
storagePath: string;
|
|
1635
|
+
menu?: IMenu<S>;
|
|
1636
|
+
actions?: AnyAction<S>[];
|
|
1637
|
+
options?: ISprinkleOptions;
|
|
1638
|
+
argv?: string[];
|
|
1639
|
+
}): Promise<void> {
|
|
1640
|
+
const argv = opts.argv ?? process.argv.slice(2);
|
|
1641
|
+
const mode = detectMode(argv);
|
|
1642
|
+
|
|
1643
|
+
if (mode === "tui") {
|
|
1644
|
+
if (!opts.menu) {
|
|
1645
|
+
throw new Error(
|
|
1646
|
+
"TUI mode requires a menu. Provide a menu option or pass --help to see available actions.",
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
const sprinkle = await Sprinkle.New(
|
|
1650
|
+
opts.type,
|
|
1651
|
+
opts.storagePath,
|
|
1652
|
+
opts.options,
|
|
1653
|
+
);
|
|
1654
|
+
// Register any provided actions
|
|
1655
|
+
for (const action of opts.actions ?? []) {
|
|
1656
|
+
sprinkle.registerAction(action);
|
|
1657
|
+
}
|
|
1658
|
+
await sprinkle.showMenu(opts.menu);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (mode === "help") {
|
|
1663
|
+
const sprinkle = new Sprinkle(opts.type, opts.storagePath, opts.options);
|
|
1664
|
+
for (const action of opts.actions ?? []) {
|
|
1665
|
+
sprinkle.registerAction(action);
|
|
1666
|
+
}
|
|
1667
|
+
console.log(generateAppHelp(sprinkle.listActions()));
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
if (mode === "cli") {
|
|
1672
|
+
const { actionName, args } = parseCliArgs(argv);
|
|
1673
|
+
|
|
1674
|
+
// Extract --profile flag from args if present
|
|
1675
|
+
const profileName =
|
|
1676
|
+
typeof args["profile"] === "string" ? args["profile"] : undefined;
|
|
1677
|
+
|
|
1678
|
+
const sprinkle = new Sprinkle(opts.type, opts.storagePath, opts.options);
|
|
1679
|
+
for (const action of opts.actions ?? []) {
|
|
1680
|
+
sprinkle.registerAction(action);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Handle action-specific --help BEFORE profile initialization
|
|
1684
|
+
// (help should work without a profile)
|
|
1685
|
+
if (args["help"] === true) {
|
|
1686
|
+
const action = sprinkle.getAction(actionName);
|
|
1687
|
+
if (action) {
|
|
1688
|
+
console.log(generateActionHelp(action));
|
|
1689
|
+
} else {
|
|
1690
|
+
console.log(`Unknown action "${actionName}". Run --help to see available actions.`);
|
|
1691
|
+
}
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
await sprinkle.initNonInteractive(profileName);
|
|
1696
|
+
|
|
1697
|
+
// Remove internal flags from args before passing to action
|
|
1698
|
+
const actionArgs = { ...args };
|
|
1699
|
+
delete actionArgs["profile"];
|
|
1700
|
+
delete actionArgs["help"];
|
|
1701
|
+
|
|
1702
|
+
// Build context and delegate to runCli for proper error routing
|
|
1703
|
+
const context = {
|
|
1704
|
+
sprinkle,
|
|
1705
|
+
settings: sprinkle.settings,
|
|
1706
|
+
};
|
|
1707
|
+
await runCli(sprinkle, actionName, actionArgs, context);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
if (mode === "mcp") {
|
|
1712
|
+
// Derive a server name from the storage path basename, falling back to a
|
|
1713
|
+
// sensible default.
|
|
1714
|
+
const serverName = path.basename(opts.storagePath) || "sprinkle-mcp";
|
|
1715
|
+
|
|
1716
|
+
const sprinkle = new Sprinkle(opts.type, opts.storagePath, opts.options);
|
|
1717
|
+
for (const action of opts.actions ?? []) {
|
|
1718
|
+
sprinkle.registerAction(action);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Read profile name from environment variable (MCP clients have no
|
|
1722
|
+
// interactive terminal to prompt for profile selection).
|
|
1723
|
+
const profileName = process.env["SPRINKLE_PROFILE"];
|
|
1724
|
+
|
|
1725
|
+
await sprinkle.initNonInteractive(profileName);
|
|
1726
|
+
await runMcp(sprinkle, serverName);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1379
1730
|
}
|
package/src/Sprinkle/prompts.ts
CHANGED
|
@@ -61,7 +61,9 @@ interface SelectConfig<T> {
|
|
|
61
61
|
theme?: PartialDeep<Theme<typeof selectTheme>>;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function isSelectable<T>(
|
|
64
|
+
function isSelectable<T>(
|
|
65
|
+
item: SelectChoice<T> | Separator,
|
|
66
|
+
): item is SelectChoice<T> {
|
|
65
67
|
return !Separator.isSeparator(item) && !item.disabled;
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -100,7 +102,10 @@ export const selectCancellable = createPrompt<
|
|
|
100
102
|
const { loop = true, pageSize = 15 } = config;
|
|
101
103
|
const theme = makeTheme(selectTheme, config.theme);
|
|
102
104
|
const [status, setStatus] = useState<"idle" | "done" | "cancelled">("idle");
|
|
103
|
-
const prefix = usePrefix({
|
|
105
|
+
const prefix = usePrefix({
|
|
106
|
+
status: status === "cancelled" ? "done" : status,
|
|
107
|
+
theme,
|
|
108
|
+
});
|
|
104
109
|
|
|
105
110
|
const items = useMemo(
|
|
106
111
|
() => normalizeChoices(config.choices),
|
|
@@ -199,7 +204,9 @@ export const selectCancellable = createPrompt<
|
|
|
199
204
|
return `${prefix} ${config.message} ${colors.cyan(selectedChoice?.short ?? selectedChoice?.name ?? "")}`;
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
const helpTip = colors.dim(
|
|
207
|
+
const helpTip = colors.dim(
|
|
208
|
+
"(Use arrow keys, Enter to select, Esc to cancel)",
|
|
209
|
+
);
|
|
203
210
|
return `${prefix} ${config.message} ${helpTip}\n${page}`;
|
|
204
211
|
});
|
|
205
212
|
|
|
@@ -291,7 +298,10 @@ export const inputCancellable = createPrompt<string | null, InputConfig>(
|
|
|
291
298
|
const [touched, setTouched] = useState(false);
|
|
292
299
|
const [error, setError] = useState<string | null>(null);
|
|
293
300
|
const theme = makeTheme(inputTheme);
|
|
294
|
-
const prefix = usePrefix({
|
|
301
|
+
const prefix = usePrefix({
|
|
302
|
+
status: status === "cancelled" ? "done" : status,
|
|
303
|
+
theme,
|
|
304
|
+
});
|
|
295
305
|
|
|
296
306
|
useKeypress(async (key, rl) => {
|
|
297
307
|
if (isEscapeKey(key)) {
|
|
@@ -356,7 +366,10 @@ export const passwordCancellable = createPrompt<string | null, PasswordConfig>(
|
|
|
356
366
|
const [value, setValue] = useState("");
|
|
357
367
|
const [error, setError] = useState<string | null>(null);
|
|
358
368
|
const theme = makeTheme(inputTheme);
|
|
359
|
-
const prefix = usePrefix({
|
|
369
|
+
const prefix = usePrefix({
|
|
370
|
+
status: status === "cancelled" ? "done" : status,
|
|
371
|
+
theme,
|
|
372
|
+
});
|
|
360
373
|
const mask = config.mask ?? "*";
|
|
361
374
|
|
|
362
375
|
useKeypress(async (key, rl) => {
|
|
@@ -418,7 +431,10 @@ export const confirmCancellable = createPrompt<boolean | null, ConfirmConfig>(
|
|
|
418
431
|
// Track value as undefined when no default, requiring explicit y/n
|
|
419
432
|
const [value, setValue] = useState<boolean | undefined>(config.default);
|
|
420
433
|
const theme = makeTheme(inputTheme);
|
|
421
|
-
const prefix = usePrefix({
|
|
434
|
+
const prefix = usePrefix({
|
|
435
|
+
status: status === "cancelled" ? "done" : status,
|
|
436
|
+
theme,
|
|
437
|
+
});
|
|
422
438
|
|
|
423
439
|
useKeypress((key) => {
|
|
424
440
|
if (isEscapeKey(key)) {
|
|
@@ -437,7 +453,12 @@ export const confirmCancellable = createPrompt<boolean | null, ConfirmConfig>(
|
|
|
437
453
|
}
|
|
438
454
|
});
|
|
439
455
|
|
|
440
|
-
const hint =
|
|
456
|
+
const hint =
|
|
457
|
+
config.default === true
|
|
458
|
+
? "(Y/n)"
|
|
459
|
+
: config.default === false
|
|
460
|
+
? "(y/N)"
|
|
461
|
+
: "(y/n)";
|
|
441
462
|
const displayValue = value === true ? "Yes" : value === false ? "No" : "";
|
|
442
463
|
|
|
443
464
|
if (status === "cancelled") {
|
|
@@ -458,7 +479,9 @@ export const confirmCancellable = createPrompt<boolean | null, ConfirmConfig>(
|
|
|
458
479
|
* Returns the selected value with proper typing, or null if cancelled.
|
|
459
480
|
*/
|
|
460
481
|
export async function select<T>(config: SelectConfig<T>): Promise<T | null> {
|
|
461
|
-
return selectCancellable(
|
|
482
|
+
return selectCancellable(
|
|
483
|
+
config as SelectConfig<unknown>,
|
|
484
|
+
) as Promise<T | null>;
|
|
462
485
|
}
|
|
463
486
|
|
|
464
487
|
interface SearchConfig<T> {
|
|
@@ -475,77 +498,100 @@ interface SearchConfig<T> {
|
|
|
475
498
|
* Note: This wraps @inquirer/search which has built-in escape handling,
|
|
476
499
|
* but we provide a consistent API with other cancellable prompts.
|
|
477
500
|
*/
|
|
478
|
-
export const searchCancellable = createPrompt<
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
setLoading(false);
|
|
497
|
-
}
|
|
498
|
-
}, [searchTerm]);
|
|
499
|
-
|
|
500
|
-
useKeypress((key, rl) => {
|
|
501
|
-
if (isEscapeKey(key)) {
|
|
502
|
-
setStatus("cancelled");
|
|
503
|
-
done(null);
|
|
504
|
-
} else if (isEnterKey(key) && results.length > 0) {
|
|
505
|
-
setStatus("done");
|
|
506
|
-
done(results[active]?.value ?? null);
|
|
507
|
-
} else if (isUpKey(key) && results.length > 0) {
|
|
508
|
-
rl.clearLine(0);
|
|
509
|
-
setActive(active > 0 ? active - 1 : results.length - 1);
|
|
510
|
-
} else if (isDownKey(key) && results.length > 0) {
|
|
511
|
-
rl.clearLine(0);
|
|
512
|
-
setActive(active < results.length - 1 ? active + 1 : 0);
|
|
513
|
-
} else if (isBackspaceKey(key)) {
|
|
514
|
-
setSearchTerm(rl.line);
|
|
515
|
-
} else if (key.name !== "tab" && !key.ctrl && !(key as any).meta) {
|
|
516
|
-
setSearchTerm(rl.line);
|
|
517
|
-
}
|
|
518
|
-
});
|
|
501
|
+
export const searchCancellable = createPrompt<
|
|
502
|
+
unknown | null,
|
|
503
|
+
SearchConfig<unknown>
|
|
504
|
+
>((config, done) => {
|
|
505
|
+
const [status, setStatus] = useState<"idle" | "done" | "cancelled">("idle");
|
|
506
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
507
|
+
const [results, setResults] = useState<{ name: string; value: unknown }[]>(
|
|
508
|
+
[],
|
|
509
|
+
);
|
|
510
|
+
const [active, setActive] = useState(0);
|
|
511
|
+
const [loading, setLoading] = useState(false);
|
|
512
|
+
// Store selected item when Enter is pressed to avoid stale closure issues
|
|
513
|
+
const [selectedName, setSelectedName] = useState<string>("");
|
|
514
|
+
const theme = makeTheme(selectTheme);
|
|
515
|
+
const prefix = usePrefix({
|
|
516
|
+
status: status === "cancelled" ? "done" : status,
|
|
517
|
+
theme,
|
|
518
|
+
});
|
|
519
519
|
|
|
520
|
-
|
|
521
|
-
|
|
520
|
+
// Fetch results when search term changes
|
|
521
|
+
useMemo(async () => {
|
|
522
|
+
setLoading(true);
|
|
523
|
+
try {
|
|
524
|
+
const items = await config.source(searchTerm || undefined);
|
|
525
|
+
setResults(items);
|
|
526
|
+
// Don't reset active here - it's reset in keypress handler when user types
|
|
527
|
+
} finally {
|
|
528
|
+
setLoading(false);
|
|
522
529
|
}
|
|
530
|
+
}, [searchTerm]);
|
|
523
531
|
|
|
524
|
-
|
|
532
|
+
useKeypress((key, rl) => {
|
|
533
|
+
if (isEscapeKey(key)) {
|
|
534
|
+
setStatus("cancelled");
|
|
535
|
+
done(null);
|
|
536
|
+
} else if (isEnterKey(key) && results.length > 0) {
|
|
525
537
|
const selected = results[active];
|
|
526
|
-
|
|
538
|
+
setSelectedName(selected?.name ?? "");
|
|
539
|
+
setStatus("done");
|
|
540
|
+
done(selected?.value ?? null);
|
|
541
|
+
} else if (isUpKey(key) && results.length > 0) {
|
|
542
|
+
setActive(active > 0 ? active - 1 : results.length - 1);
|
|
543
|
+
} else if (isDownKey(key) && results.length > 0) {
|
|
544
|
+
setActive(active < results.length - 1 ? active + 1 : 0);
|
|
545
|
+
} else if (isBackspaceKey(key)) {
|
|
546
|
+
// Reset active when user modifies search term
|
|
547
|
+
setActive(0);
|
|
548
|
+
setSearchTerm(rl.line);
|
|
549
|
+
} else if (
|
|
550
|
+
key.name !== "tab" &&
|
|
551
|
+
key.name !== "return" &&
|
|
552
|
+
key.name !== "enter" &&
|
|
553
|
+
key.name !== "up" &&
|
|
554
|
+
key.name !== "down" &&
|
|
555
|
+
key.name !== "left" &&
|
|
556
|
+
key.name !== "right" &&
|
|
557
|
+
key.name !== "escape" &&
|
|
558
|
+
key.name !== "backspace" &&
|
|
559
|
+
!key.ctrl &&
|
|
560
|
+
!(key as any).meta
|
|
561
|
+
) {
|
|
562
|
+
// Reset active when user types new characters (not navigation/action keys)
|
|
563
|
+
setActive(0);
|
|
564
|
+
setSearchTerm(rl.line);
|
|
527
565
|
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
if (status === "cancelled") {
|
|
569
|
+
return `${prefix} ${config.message} ${colors.dim("(cancelled)")}`;
|
|
570
|
+
}
|
|
528
571
|
|
|
529
|
-
|
|
530
|
-
|
|
572
|
+
if (status === "done") {
|
|
573
|
+
return `${prefix} ${config.message} ${colors.cyan(selectedName)}`;
|
|
574
|
+
}
|
|
531
575
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
576
|
+
const helpTip = colors.dim("(Type to search, Esc to cancel)");
|
|
577
|
+
const searchLine = `${prefix} ${config.message} ${helpTip} ${searchTerm}`;
|
|
535
578
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
579
|
+
if (loading) {
|
|
580
|
+
return `${searchLine}\n${colors.dim(" Searching...")}`;
|
|
581
|
+
}
|
|
539
582
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const cursor = i === active ? figures.pointer : " ";
|
|
544
|
-
const color = i === active ? colors.cyan : (x: string) => x;
|
|
545
|
-
return color(`${cursor} ${item.name}`);
|
|
546
|
-
})
|
|
547
|
-
.join("\n");
|
|
583
|
+
if (results.length === 0) {
|
|
584
|
+
return `${searchLine}\n${colors.dim(" No results")}`;
|
|
585
|
+
}
|
|
548
586
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
)
|
|
587
|
+
const resultLines = results
|
|
588
|
+
.slice(0, 7)
|
|
589
|
+
.map((item, i) => {
|
|
590
|
+
const cursor = i === active ? figures.pointer : " ";
|
|
591
|
+
const color = i === active ? colors.cyan : (x: string) => x;
|
|
592
|
+
return color(`${cursor} ${item.name}`);
|
|
593
|
+
})
|
|
594
|
+
.join("\n");
|
|
595
|
+
|
|
596
|
+
return `${searchLine}\n${resultLines}`;
|
|
597
|
+
});
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
import {
|
|
7
7
|
Kind,
|
|
8
8
|
type TBigInt,
|
|
9
|
+
type TBoolean,
|
|
10
|
+
type TInteger,
|
|
9
11
|
type TLiteral,
|
|
10
12
|
type TNull,
|
|
13
|
+
type TNumber,
|
|
11
14
|
type TObject,
|
|
12
15
|
type TSchema,
|
|
13
16
|
type TString,
|
|
@@ -31,6 +34,12 @@ export const isArray = (t: TSchema): t is TArray => t[Kind] === "Array";
|
|
|
31
34
|
|
|
32
35
|
export const isBigInt = (t: TSchema): t is TBigInt => t[Kind] === "BigInt";
|
|
33
36
|
|
|
37
|
+
export const isBoolean = (t: TSchema): t is TBoolean => t[Kind] === "Boolean";
|
|
38
|
+
|
|
39
|
+
export const isInteger = (t: TSchema): t is TInteger => t[Kind] === "Integer";
|
|
40
|
+
|
|
41
|
+
export const isNumber = (t: TSchema): t is TNumber => t[Kind] === "Number";
|
|
42
|
+
|
|
34
43
|
export const isLiteral = (t: TSchema): t is TLiteral => t[Kind] === "Literal";
|
|
35
44
|
|
|
36
45
|
export const isObject = (t: TSchema): t is TObject => t[Kind] === "Object";
|