@sundaeswap/sprinkles 0.6.0 → 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__/fill-in-struct.test.js +138 -0
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- 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 +451 -4
- 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__/fill-in-struct.test.js +138 -0
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- 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 +299 -4
- 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__/fill-in-struct.test.ts +144 -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 +395 -3
- package/src/Sprinkle/prompts.ts +118 -72
- package/src/Sprinkle/type-guards.ts +9 -0
package/src/Sprinkle/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./prompts.js";
|
|
20
20
|
import colors from "yoctocolors-cjs";
|
|
21
21
|
import { type TSchema, Type, OptionalKind } from "@sinclair/typebox";
|
|
22
|
+
import { Value } from "@sinclair/typebox/value";
|
|
22
23
|
import * as fs from "fs";
|
|
23
24
|
import * as path from "path";
|
|
24
25
|
export * from "@sinclair/typebox";
|
|
@@ -68,7 +69,10 @@ import {
|
|
|
68
69
|
isImport,
|
|
69
70
|
isArray,
|
|
70
71
|
isBigInt,
|
|
72
|
+
isBoolean,
|
|
73
|
+
isInteger,
|
|
71
74
|
isLiteral,
|
|
75
|
+
isNumber,
|
|
72
76
|
isObject,
|
|
73
77
|
isRef,
|
|
74
78
|
isString,
|
|
@@ -87,7 +91,10 @@ export {
|
|
|
87
91
|
isImport,
|
|
88
92
|
isArray,
|
|
89
93
|
isBigInt,
|
|
94
|
+
isBoolean,
|
|
95
|
+
isInteger,
|
|
90
96
|
isLiteral,
|
|
97
|
+
isNumber,
|
|
91
98
|
isObject,
|
|
92
99
|
isRef,
|
|
93
100
|
isString,
|
|
@@ -145,6 +152,47 @@ import {
|
|
|
145
152
|
import { promptObject, promptArray } from "./menus/index.js";
|
|
146
153
|
import { formatPath } from "./utils/formatting.js";
|
|
147
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
|
+
|
|
148
196
|
export interface IMenuAction<S extends TSchema> {
|
|
149
197
|
title: string;
|
|
150
198
|
action: (sprinkle: Sprinkle<S>) => Promise<Sprinkle<S> | void>;
|
|
@@ -164,11 +212,13 @@ export class Sprinkle<S extends TSchema> {
|
|
|
164
212
|
options: ISprinkleOptions;
|
|
165
213
|
profileId: string = "";
|
|
166
214
|
profileMeta: IProfileMeta = { name: "", createdAt: "", updatedAt: "" };
|
|
215
|
+
actionRegistry: ActionRegistry<S>;
|
|
167
216
|
|
|
168
217
|
constructor(type: S, storagePath: string, options?: ISprinkleOptions) {
|
|
169
218
|
this.type = type;
|
|
170
219
|
this.storagePath = storagePath;
|
|
171
220
|
this.options = options ?? {};
|
|
221
|
+
this.actionRegistry = new ActionRegistry<S>();
|
|
172
222
|
}
|
|
173
223
|
|
|
174
224
|
// --- Current Profile Accessor ---
|
|
@@ -590,8 +640,15 @@ export class Sprinkle<S extends TSchema> {
|
|
|
590
640
|
{
|
|
591
641
|
title: "Edit settings",
|
|
592
642
|
action: async () => {
|
|
593
|
-
|
|
594
|
-
|
|
643
|
+
try {
|
|
644
|
+
this.settings = await this.EditStruct(this.type, this.settings);
|
|
645
|
+
this.saveSettings();
|
|
646
|
+
} catch (e) {
|
|
647
|
+
if (e instanceof UserCancelledError) {
|
|
648
|
+
return; // User cancelled, return to menu
|
|
649
|
+
}
|
|
650
|
+
throw e;
|
|
651
|
+
}
|
|
595
652
|
},
|
|
596
653
|
},
|
|
597
654
|
{
|
|
@@ -763,6 +820,104 @@ export class Sprinkle<S extends TSchema> {
|
|
|
763
820
|
return maskSensitiveFields(this.settings, this.type);
|
|
764
821
|
}
|
|
765
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
|
+
|
|
766
921
|
async TxDialog<P extends Provider, W extends Wallet>(
|
|
767
922
|
blaze: Blaze<P, W>,
|
|
768
923
|
tx: Core.Transaction,
|
|
@@ -1137,7 +1292,40 @@ export class Sprinkle<S extends TSchema> {
|
|
|
1137
1292
|
throw new UserCancelledError();
|
|
1138
1293
|
}
|
|
1139
1294
|
const selection = selectionResult as TSchema;
|
|
1140
|
-
|
|
1295
|
+
// Determine if the provided default value matches the selected variant.
|
|
1296
|
+
// For discriminated unions (objects with literal fields like `type`), check
|
|
1297
|
+
// if the literal field values in the selected variant match those in `def`.
|
|
1298
|
+
// For non-discriminated unions, fall back to structural matching with Value.Check.
|
|
1299
|
+
let matchedDef: unknown = undefined;
|
|
1300
|
+
if (def !== undefined) {
|
|
1301
|
+
if (isObject(selection)) {
|
|
1302
|
+
// Check if all literal fields in the selected variant match def
|
|
1303
|
+
const literalFields = Object.entries(selection.properties ?? {}).filter(
|
|
1304
|
+
([, fieldSchema]) => isLiteral(fieldSchema as TSchema),
|
|
1305
|
+
);
|
|
1306
|
+
if (literalFields.length > 0) {
|
|
1307
|
+
const defRecord = def as Record<string, unknown>;
|
|
1308
|
+
const allLiteralsMatch = literalFields.every(
|
|
1309
|
+
([fieldName, fieldSchema]) =>
|
|
1310
|
+
defRecord[fieldName] === (fieldSchema as unknown as { const: unknown }).const,
|
|
1311
|
+
);
|
|
1312
|
+
if (allLiteralsMatch) {
|
|
1313
|
+
matchedDef = def;
|
|
1314
|
+
}
|
|
1315
|
+
} else {
|
|
1316
|
+
// No literal discriminators - use structural check
|
|
1317
|
+
if (Value.Check(selection, def)) {
|
|
1318
|
+
matchedDef = def;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
} else {
|
|
1322
|
+
// Non-object variant - use structural check
|
|
1323
|
+
if (Value.Check(selection, def)) {
|
|
1324
|
+
matchedDef = def;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return this._fillInStruct(selection, path, defs, matchedDef as TExact<typeof selection>) as Promise<TExact<U>>;
|
|
1141
1329
|
}
|
|
1142
1330
|
|
|
1143
1331
|
if (isString(type)) {
|
|
@@ -1335,4 +1523,208 @@ export class Sprinkle<S extends TSchema> {
|
|
|
1335
1523
|
|
|
1336
1524
|
return def;
|
|
1337
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
|
+
}
|
|
1338
1730
|
}
|