@forgerock/login-framework-cli 0.0.0-beta.0 → 0.1.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.
Files changed (51) hide show
  1. package/README.md +107 -34
  2. package/dist/{commands → src/commands}/generate.d.ts +3 -2
  3. package/dist/{commands → src/commands}/generate.js +15 -8
  4. package/dist/src/commands/init.d.ts +16 -0
  5. package/dist/{commands → src/commands}/init.js +8 -10
  6. package/dist/{commands → src/commands}/source.d.ts +1 -1
  7. package/dist/{commands → src/commands}/update.d.ts +4 -2
  8. package/dist/{commands → src/commands}/update.js +7 -6
  9. package/dist/{config → src/config}/version.d.ts +1 -4
  10. package/dist/{config → src/config}/version.js +1 -2
  11. package/dist/src/main.js +61 -0
  12. package/dist/src/mcp.d.ts +52 -0
  13. package/dist/src/mcp.js +182 -0
  14. package/dist/{services → src/services}/registry.js +2 -2
  15. package/dist/{services → src/services}/release.d.ts +2 -1
  16. package/dist/src/templates/callback/__COMPONENT_SLUG__.mock.ts +22 -0
  17. package/dist/src/templates/callback/__COMPONENT_SLUG__.stories.js +28 -0
  18. package/dist/src/templates/callback/__COMPONENT_SLUG__.story.svelte +59 -0
  19. package/dist/src/templates/callback/__COMPONENT_SLUG__.svelte +49 -0
  20. package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +9 -0
  21. package/dist/{templates → src/templates}/callback/__COMPONENT_SLUG__.utilities.ts +1 -1
  22. package/dist/src/templates/stage/__COMPONENT_SLUG__.mock.ts +23 -0
  23. package/dist/src/templates/stage/__COMPONENT_SLUG__.stories.js +43 -0
  24. package/dist/src/templates/stage/__COMPONENT_SLUG__.story.svelte +47 -0
  25. package/dist/{templates → src/templates}/stage/__COMPONENT_SLUG__.svelte +21 -19
  26. package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +9 -0
  27. package/dist/{templates → src/templates}/stage/__COMPONENT_SLUG__.utilities.ts +1 -1
  28. package/dist/src/templates/tsconfig.json +18 -0
  29. package/dist/src/utils.d.ts +11 -0
  30. package/dist/src/utils.js +17 -0
  31. package/package.json +13 -10
  32. package/dist/commands/init.d.ts +0 -8
  33. package/dist/main.js +0 -34
  34. package/dist/templates/callback/__COMPONENT_SLUG__.svelte +0 -73
  35. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +0 -9
  36. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +0 -9
  37. package/dist/utils.d.ts +0 -2
  38. package/dist/utils.js +0 -4
  39. package/scripts/generate-registry.mjs +0 -23
  40. package/dist/{commands → src/commands}/releases.d.ts +0 -0
  41. package/dist/{commands → src/commands}/releases.js +0 -0
  42. package/dist/{commands → src/commands}/source.js +0 -0
  43. package/dist/{config → src/config}/exclusions.d.ts +0 -0
  44. package/dist/{config → src/config}/exclusions.js +0 -0
  45. package/dist/{errors.d.ts → src/errors.d.ts} +8 -8
  46. package/dist/{errors.js → src/errors.js} +2 -2
  47. package/dist/{main.d.ts → src/main.d.ts} +0 -0
  48. package/dist/{services → src/services}/file-system.d.ts +1 -1
  49. package/dist/{services → src/services}/file-system.js +2 -2
  50. package/dist/{services → src/services}/registry.d.ts +1 -1
  51. package/dist/{services → src/services}/release.js +1 -1
@@ -0,0 +1,182 @@
1
+ import { McpServer, Tool, Toolkit } from '@effect/ai';
2
+ import { Command } from '@effect/cli';
3
+ import { NodeContext, NodeSink, NodeStream } from '@effect/platform-node';
4
+ import { Cause, Effect, Layer, Logger, Option, Schema } from 'effect';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { dirname, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { scaffoldComponent } from './commands/generate.js';
9
+ import { initProject } from './commands/init.js';
10
+ import { resolveSource } from './commands/source.js';
11
+ import { assertValidProject, writeVersion } from './config/version.js';
12
+ import { copyWithExclusions, expandTilde } from './services/file-system.js';
13
+ import { runRegistryScript } from './services/registry.js';
14
+ import { GithubReleaseLayer, Release } from './services/release.js';
15
+ // Resolve package.json from either dist/src/ (compiled) or src/ (Vitest).
16
+ const __dir = dirname(fileURLToPath(import.meta.url));
17
+ const pkgPath = existsSync(resolve(__dir, '../../package.json'))
18
+ ? resolve(__dir, '../../package.json')
19
+ : resolve(__dir, '../package.json');
20
+ const { version } = JSON.parse(readFileSync(pkgPath, 'utf8'));
21
+ // ── Shared error formatter ────────────────────────────────────────────────────
22
+ function formatError(cause) {
23
+ const err = Cause.failureOption(cause);
24
+ if (Option.isSome(err)) {
25
+ const e = err.value;
26
+ switch (e._tag) {
27
+ case 'InvalidVersionError':
28
+ return `Invalid version "${e['version']}". Expected semver format like v1.0.0.`;
29
+ case 'ReleaseNetworkError':
30
+ return `Network error reaching GitHub: ${e['cause']}`;
31
+ case 'ReleaseParseError':
32
+ return `Failed to parse release data: ${e['cause']}`;
33
+ case 'ReleaseFsError':
34
+ return `Filesystem error during ${e['operation']}: ${e['cause']}`;
35
+ case 'ReleaseNotFoundError':
36
+ return `No releases found on GitHub${e['cause'] ? `: ${e['cause']}` : ''}.`;
37
+ case 'FileSystemError':
38
+ return `Filesystem error (${e['operation']}) at "${e['path']}": ${e['cause']}`;
39
+ case 'GeneratorVersionError':
40
+ return `Generator version error: ${e['message']}${e['path'] ? ` (${e['path']})` : ''}`;
41
+ case 'RegistryScanError':
42
+ return `Registry scan failed in "${e['directory']}": ${e['cause']}`;
43
+ case 'DirectoryConflictError':
44
+ return `"${e['path']}" already contains a framework project. Use local path instead.`;
45
+ case 'DirectoryNotEmptyError':
46
+ return `"${e['path']}" already exists and is not empty.`;
47
+ case 'InvalidComponentNameError':
48
+ return `"${e['name']}" is not a valid component name. Use PascalCase (e.g. MyCallback).`;
49
+ case 'ComponentAlreadyExistsError':
50
+ return `Component directory already exists: ${e['path']}`;
51
+ default:
52
+ try {
53
+ return `Error: ${JSON.stringify(e, (_k, v) => v instanceof Error ? { message: v.message, name: v.name } : v)}`;
54
+ }
55
+ catch {
56
+ return `Error: ${String(e)}`;
57
+ }
58
+ }
59
+ }
60
+ return `Unexpected error: ${Cause.pretty(cause)}`;
61
+ }
62
+ const toolLayer = Layer.mergeAll(GithubReleaseLayer, NodeContext.layer);
63
+ const catchToolErrors = (effect) => effect.pipe(Effect.provide(toolLayer), Effect.catchAll((err) => Effect.fail(formatError(Cause.fail(err)))));
64
+ // ── Tool definitions ─────────────────────────────────────────────────────────
65
+ const InitTool = Tool.make('init', {
66
+ description: 'Bootstrap a new Ping Login Widget / Login App project from a GitHub release or local path.',
67
+ parameters: {
68
+ directory: Schema.String.annotations({
69
+ description: 'Directory to initialize the project in (e.g. "my-project" or "./")',
70
+ }),
71
+ version: Schema.optional(Schema.String).annotations({
72
+ description: 'Framework version tag to download (e.g. v1.2.0). Defaults to latest.',
73
+ }),
74
+ local: Schema.optional(Schema.String).annotations({
75
+ description: 'Path to a local framework directory instead of downloading from GitHub.',
76
+ }),
77
+ },
78
+ success: Schema.String,
79
+ failure: Schema.String,
80
+ })
81
+ .annotate(Tool.Destructive, true)
82
+ .annotate(Tool.OpenWorld, true)
83
+ .annotate(Tool.Idempotent, false);
84
+ const directoryParam = Schema.optional(Schema.String).annotations({
85
+ description: 'Absolute path to the initialized project root. Defaults to the current working directory.',
86
+ });
87
+ const GenerateCallbackTool = Tool.make('generate_callback', {
88
+ description: 'Scaffold a new custom callback component under experimental/custom/callbacks/. Run from an initialized project root.',
89
+ parameters: {
90
+ name: Schema.String.annotations({
91
+ description: 'PascalCase component name (e.g. MyCallback). Must match the AM callback type string.',
92
+ }),
93
+ directory: directoryParam,
94
+ },
95
+ success: Schema.String,
96
+ failure: Schema.String,
97
+ })
98
+ .annotate(Tool.Destructive, true)
99
+ .annotate(Tool.OpenWorld, false)
100
+ .annotate(Tool.Idempotent, false);
101
+ const GenerateStageTool = Tool.make('generate_stage', {
102
+ description: 'Scaffold a new custom stage component under experimental/custom/stages/. Run from an initialized project root.',
103
+ parameters: {
104
+ name: Schema.String.annotations({
105
+ description: 'Stage name as configured on the AM journey Page Node (e.g. "DefaultLogin" or "My Login Stage").',
106
+ }),
107
+ directory: directoryParam,
108
+ },
109
+ success: Schema.String,
110
+ failure: Schema.String,
111
+ })
112
+ .annotate(Tool.Destructive, true)
113
+ .annotate(Tool.OpenWorld, false)
114
+ .annotate(Tool.Idempotent, false);
115
+ const UpdateTool = Tool.make('update', {
116
+ description: 'Fetch the latest (or specified) framework version and overwrite core files while preserving experimental/custom/. Run from an initialized project root.',
117
+ parameters: {
118
+ directory: directoryParam,
119
+ version: Schema.optional(Schema.String).annotations({
120
+ description: 'Framework version tag to update to (e.g. v1.2.0). Defaults to latest.',
121
+ }),
122
+ local: Schema.optional(Schema.String).annotations({
123
+ description: 'Path to a local framework directory instead of downloading from GitHub.',
124
+ }),
125
+ },
126
+ success: Schema.String,
127
+ failure: Schema.String,
128
+ })
129
+ .annotate(Tool.Destructive, true)
130
+ .annotate(Tool.OpenWorld, true)
131
+ .annotate(Tool.Idempotent, false);
132
+ const ListReleasesTool = Tool.make('list_releases', {
133
+ description: 'List available Login Framework releases from GitHub.',
134
+ parameters: {},
135
+ success: Schema.String,
136
+ failure: Schema.String,
137
+ })
138
+ .annotate(Tool.Readonly, true)
139
+ .annotate(Tool.Destructive, false)
140
+ .annotate(Tool.OpenWorld, true)
141
+ .annotate(Tool.Idempotent, true);
142
+ // ── Exported toolkit (used in tests) ─────────────────────────────────────────
143
+ export const mcpToolkit = Toolkit.make(InitTool, GenerateCallbackTool, GenerateStageTool, UpdateTool, ListReleasesTool);
144
+ // ── Tool handlers ─────────────────────────────────────────────────────────────
145
+ const handlerLayer = mcpToolkit.toLayer({
146
+ init: ({ directory, version: ver, local }) => catchToolErrors(initProject({
147
+ directory,
148
+ version: Option.fromNullable(ver),
149
+ local: Option.fromNullable(local),
150
+ }).pipe(Effect.map(() => `Project initialized successfully in "${directory}".`))),
151
+ generate_callback: ({ name, directory }) => catchToolErrors(scaffoldComponent('callback', name, directory).pipe(Effect.map(() => `Callback component "${name}" scaffolded successfully.`))),
152
+ generate_stage: ({ name, directory }) => catchToolErrors(scaffoldComponent('stage', name, directory).pipe(Effect.map(() => `Stage component "${name}" scaffolded successfully.`))),
153
+ update: ({ version: ver, local, directory }) => catchToolErrors(Effect.gen(function* () {
154
+ const cwd = resolve(expandTilde(directory ?? process.cwd()));
155
+ const currentVersion = yield* assertValidProject(cwd);
156
+ const resolvedVersion = yield* Effect.scoped(Effect.gen(function* () {
157
+ const { sourceDir, resolvedVersion } = yield* resolveSource(Option.fromNullable(local), Option.fromNullable(ver), cwd);
158
+ yield* copyWithExclusions(sourceDir, cwd);
159
+ return resolvedVersion;
160
+ }));
161
+ yield* runRegistryScript(cwd);
162
+ yield* writeVersion(cwd, {
163
+ version: resolvedVersion,
164
+ generatedAt: new Date().toISOString(),
165
+ });
166
+ return `Updated from ${currentVersion.version} to ${resolvedVersion}. Run "pnpm install" if dependencies changed.`;
167
+ })),
168
+ list_releases: () => catchToolErrors(Release.pipe(Effect.flatMap((release) => release.listReleases()), Effect.map((releases) => releases.map(({ tag, publishedAt }) => `${tag.padEnd(10)} ${publishedAt}`).join('\n')))),
169
+ });
170
+ // ── Server layer ──────────────────────────────────────────────────────────────
171
+ const ServerLayer = McpServer.toolkit(mcpToolkit).pipe(Layer.provide(handlerLayer), Layer.provide(Layer.mergeAll(McpServer.layerStdio({
172
+ name: 'ping-lf',
173
+ version,
174
+ stdin: NodeStream.stdin,
175
+ stdout: NodeSink.stdout,
176
+ }), NodeContext.layer, Logger.add(Logger.prettyLogger({ stderr: true })))));
177
+ export { ServerLayer };
178
+ // mcpCommand is registered only so `ping-lf --help` lists `mcp` as a subcommand.
179
+ // The actual MCP dispatch happens in main.ts via Layer.launch(ServerLayer) at the
180
+ // top level, outside the @effect/cli command handler where Layer.launch would
181
+ // nest a second fiber scope inside the CLI's already-running one.
182
+ export const mcpCommand = Command.make('mcp', {}, () => Effect.void).pipe(Command.withDescription('Start as an MCP server over stdio.'));
@@ -1,5 +1,5 @@
1
- import { Console, Effect } from 'effect';
2
1
  import { FileSystem, Path } from '@effect/platform';
2
+ import { Console, Effect } from 'effect';
3
3
  import { RegistryScanError } from '../errors.js';
4
4
  // --------------------------------------------------------------------------
5
5
  // Helpers (exported for testing)
@@ -138,7 +138,7 @@ export const runRegistryScript = (projectDir) => Effect.gen(function* () {
138
138
  const path = yield* Path.Path;
139
139
  const stageDir = path.join(projectDir, 'experimental', 'custom', 'stages');
140
140
  const callbackDir = path.join(projectDir, 'experimental', 'custom', 'callbacks');
141
- const registryDir = path.join(projectDir, 'core', 'journey', '_utilities');
141
+ const registryDir = path.join(projectDir, 'core', 'journey', '_utilities', 'registry');
142
142
  const registryPath = path.join(registryDir, 'custom-registry.ts');
143
143
  const [stageComponents, callbackComponents] = yield* Effect.all([
144
144
  scanDirectory(fs, path, stageDir, 'stage'),
@@ -1,6 +1,7 @@
1
- import { Context, Effect, Layer, Scope } from 'effect';
2
1
  import { FileSystem, HttpClient } from '@effect/platform';
2
+ import { Context, Effect, Layer } from 'effect';
3
3
  import { InvalidVersionError, ReleaseFsError, ReleaseNetworkError, ReleaseNotFoundError, ReleaseParseError } from '../errors.js';
4
+ import type { Scope } from 'effect';
4
5
  export declare function validateVersion(version: string): Effect.Effect<string, InvalidVersionError>;
5
6
  /**
6
7
  * Parses a GitHub releases API JSON response and returns an array of
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Mock journey step for the __COMPONENT_NAME__ callback's Storybook stories.
3
+ *
4
+ * Replace the placeholder `type`, `output`, and `input` fields with the real
5
+ * AM payload your callback consumes. The shape mirrors what AM sends back from
6
+ * an authentication tree.
7
+ */
8
+
9
+ import { createJourneyStep } from '$login-framework';
10
+
11
+ export default createJourneyStep({
12
+ authId: 'test-auth-id',
13
+ callbacks: [
14
+ {
15
+ type: 'NameCallback',
16
+ output: [{ name: 'prompt', value: '__COMPONENT_NAME__ prompt' }],
17
+ input: [{ name: 'IDToken1', value: '' }],
18
+ _id: 0,
19
+ },
20
+ ],
21
+ stage: 'DefaultLogin',
22
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Storybook stories for the __COMPONENT_NAME__ callback.
3
+ *
4
+ * Update the `getCallback` helper if your mock contains more than one callback,
5
+ * or add additional stories for variants (loading, error, prefilled, etc.).
6
+ */
7
+
8
+ import step from './__COMPONENT_SLUG__.mock';
9
+ import Story from './__COMPONENT_SLUG__.story.svelte';
10
+
11
+ export default {
12
+ argTypes: {
13
+ callback: { control: false },
14
+ },
15
+ component: Story,
16
+ parameters: {
17
+ layout: 'fullscreen',
18
+ },
19
+ title: 'Custom/Callback/__COMPONENT_NAME__',
20
+ };
21
+
22
+ const getCallback = () => step.callbacks[0];
23
+
24
+ export const Base = {
25
+ args: {
26
+ callback: getCallback(),
27
+ },
28
+ };
@@ -0,0 +1,59 @@
1
+ <!--
2
+ Story wrapper for the __COMPONENT_NAME__ callback. Mounts the component
3
+ with default metadata so it can render in isolation inside Storybook.
4
+ -->
5
+
6
+ <script lang="ts">
7
+ import {
8
+ type BaseCallback,
9
+ type CallbackMetadata,
10
+ Centered,
11
+ type Maybe,
12
+ type SelfSubmitFunction,
13
+ type StepMetadata,
14
+ type StyleObject,
15
+ } from '$login-framework';
16
+ import __COMPONENT_NAME_PASCAL__ from './__COMPONENT_SLUG__.svelte';
17
+
18
+ export let callback: BaseCallback;
19
+ export let callbackMetadata: Maybe<CallbackMetadata> = undefined;
20
+ export let style: StyleObject = {};
21
+ export let selfSubmitFunction: Maybe<SelfSubmitFunction> = null;
22
+ export let stepMetadata: Maybe<StepMetadata> = null;
23
+
24
+ const defaultCallbackMetadata: CallbackMetadata = {
25
+ derived: {
26
+ canForceUserInputOptionality: false,
27
+ isFirstInvalidInput: false,
28
+ isReadyForSubmission: false,
29
+ isSelfSubmitting: false,
30
+ isUserInputRequired: true,
31
+ autocompleteValues: undefined,
32
+ },
33
+ idx: 0,
34
+ };
35
+
36
+ const defaultStepMetadata: StepMetadata = {
37
+ derived: {
38
+ isStepSelfSubmittable: () => false,
39
+ isUserInputOptional: false,
40
+ numOfCallbacks: 1,
41
+ numOfSelfSubmittableCbs: 0,
42
+ numOfUserInputCbs: 1,
43
+ },
44
+ };
45
+
46
+ $: mergedCallbackMetadata = { ...defaultCallbackMetadata, ...callbackMetadata };
47
+ $: mergedStepMetadata = stepMetadata ?? defaultStepMetadata;
48
+ </script>
49
+
50
+ <Centered>
51
+ <svelte:component
52
+ this={__COMPONENT_NAME_PASCAL__}
53
+ {callback}
54
+ callbackMetadata={mergedCallbackMetadata}
55
+ {style}
56
+ {selfSubmitFunction}
57
+ stepMetadata={mergedStepMetadata}
58
+ />
59
+ </Centered>
@@ -0,0 +1,49 @@
1
+ <!--
2
+ @component
3
+ Type: callback
4
+ Name: __COMPONENT_NAME__
5
+
6
+ Custom callback component. Replace this description with your own.
7
+ -->
8
+
9
+ <script lang="ts">
10
+ import type { BaseCallback } from '@forgerock/journey-client/types';
11
+
12
+ /**
13
+ * The callback instance for this component. Use `callback.getInputValue()`
14
+ * and `callback.setInputValue()` to read/write values sent back to the server.
15
+ */
16
+ export let callback: BaseCallback;
17
+
18
+ // Optionally declare any of these if your component needs them:
19
+ // import type { CallbackMetadata, Maybe, SelfSubmitFunction, StepMetadata, StyleObject } from '$login-framework';
20
+ // export let selfSubmitFunction: Maybe<SelfSubmitFunction> = null;
21
+ // export let stepMetadata: Maybe<StepMetadata> = null;
22
+ // export let callbackMetadata: Maybe<CallbackMetadata> = null;
23
+ // export let style: StyleObject = {};
24
+
25
+ // Suppress the "unused export" warning — remove `void` once you use `callback`.
26
+ void callback;
27
+ </script>
28
+
29
+ <!-- Replace the markup below with your custom callback UI. -->
30
+ <div
31
+ class="__COMPONENT_SLUG__ tw_p-4 tw_rounded-lg tw_border tw_border-secondary-light dark:tw_border-secondary-dark"
32
+ >
33
+ <p class="tw_text-base tw_text-secondary-dark dark:tw_text-secondary-light">
34
+ __COMPONENT_NAME__ callback — replace this with your implementation.
35
+ </p>
36
+ </div>
37
+
38
+ <!--
39
+ Scoped styles for this callback.
40
+ These styles only apply to this component — they won't leak to the rest of the widget.
41
+ You can also use Tailwind utility classes (prefixed with `tw_`) in the markup above.
42
+ -->
43
+ <style>
44
+ .__COMPONENT_SLUG__ {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 0.5rem;
48
+ }
49
+ </style>
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { format__COMPONENT_NAME_PASCAL__Label } from './__COMPONENT_SLUG__.utilities.js';
4
+
5
+ describe('__COMPONENT_NAME__ utilities', () => {
6
+ it('trims whitespace from label', () => {
7
+ expect(format__COMPONENT_NAME_PASCAL__Label(' hello ')).toBe('hello');
8
+ });
9
+ });
@@ -8,6 +8,6 @@
8
8
  /**
9
9
  * Example utility — replace with your own helpers.
10
10
  */
11
- export function format__COMPONENT_NAME__Label(value: string): string {
11
+ export function format__COMPONENT_NAME_PASCAL__Label(value: string): string {
12
12
  return value.trim();
13
13
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Mock journey step for the __COMPONENT_NAME__ stage's Storybook stories.
3
+ *
4
+ * A stage receives a full JourneyStep with one or more callbacks. Replace the
5
+ * placeholder callback entries below with the real shape AM sends for the
6
+ * page node this stage renders. Add more callbacks if your stage composes
7
+ * multiple inputs.
8
+ */
9
+
10
+ import { createJourneyStep } from '$login-framework';
11
+
12
+ export default createJourneyStep({
13
+ authId: 'test-auth-id',
14
+ callbacks: [
15
+ {
16
+ type: 'NameCallback',
17
+ output: [{ name: 'prompt', value: 'User Name' }],
18
+ input: [{ name: 'IDToken1', value: '' }],
19
+ _id: 0,
20
+ },
21
+ ],
22
+ stage: '__COMPONENT_NAME__',
23
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Storybook stories for the __COMPONENT_NAME__ stage.
3
+ *
4
+ * Add additional stories for variants (loading, with form error, etc.) by
5
+ * cloning the `Base` args and overriding the relevant fields.
6
+ */
7
+
8
+ import { fn } from 'storybook/test';
9
+ import { writable } from 'svelte/store';
10
+
11
+ import step from './__COMPONENT_SLUG__.mock';
12
+ import Story from './__COMPONENT_SLUG__.story.svelte';
13
+
14
+ export default {
15
+ argTypes: {
16
+ form: { control: false },
17
+ journey: { control: false },
18
+ step: { control: false },
19
+ },
20
+ component: Story,
21
+ parameters: {
22
+ layout: 'fullscreen',
23
+ },
24
+ title: 'Custom/Stage/__COMPONENT_NAME__',
25
+ };
26
+
27
+ export const Base = {
28
+ args: {
29
+ form: {
30
+ icon: true,
31
+ message: '',
32
+ status: '',
33
+ submit: fn(),
34
+ },
35
+ journey: {
36
+ loading: false,
37
+ pop: fn(),
38
+ push: fn(),
39
+ stack: writable([]),
40
+ },
41
+ step,
42
+ },
43
+ };
@@ -0,0 +1,47 @@
1
+ <!--
2
+ Story wrapper for the __COMPONENT_NAME__ stage. Builds the metadata structure
3
+ the stage expects and mounts the component inside <Centered> so it can render
4
+ in isolation inside Storybook.
5
+ -->
6
+
7
+ <script lang="ts">
8
+ import {
9
+ buildCallbackMetadata,
10
+ buildStepMetadata,
11
+ Centered,
12
+ initCheckValidation,
13
+ initializeLinks,
14
+ initializeStyles,
15
+ type JourneyStep,
16
+ type StageFormObject,
17
+ type StageJourneyObject,
18
+ type StyleObject,
19
+ } from '$login-framework';
20
+ import __COMPONENT_NAME_PASCAL__ from './__COMPONENT_SLUG__.svelte';
21
+
22
+ export let form: StageFormObject;
23
+ export let journey: StageJourneyObject;
24
+ export let step: JourneyStep;
25
+ export let style: StyleObject = {};
26
+
27
+ const callbackMetadata = buildCallbackMetadata(step, initCheckValidation());
28
+ const stepMetadata = buildStepMetadata(callbackMetadata, undefined, step.getStage());
29
+ const metadata = {
30
+ callbacks: callbackMetadata,
31
+ step: stepMetadata,
32
+ };
33
+
34
+ initializeLinks({ termsAndConditions: '/' });
35
+ initializeStyles(style);
36
+ </script>
37
+
38
+ <Centered>
39
+ <svelte:component
40
+ this={__COMPONENT_NAME_PASCAL__}
41
+ componentStyle="modal"
42
+ {form}
43
+ {journey}
44
+ {metadata}
45
+ {step}
46
+ />
47
+ </Centered>
@@ -6,35 +6,37 @@ Name: __COMPONENT_NAME__
6
6
  Custom stage component. Replace this description with your own.
7
7
 
8
8
  A stage controls the layout and submission behaviour of an entire authentication
9
- step (page node). It receives the full FRStep, maps each callback to its
9
+ step (page node). It receives the full JourneyStep, maps each callback to its
10
10
  component via CallbackMapper, and renders the form chrome (header, alerts,
11
11
  submit button, links).
12
12
  -->
13
13
 
14
14
  <script lang="ts">
15
- import type { FRStep } from '@forgerock/javascript-sdk';
16
15
  import { afterUpdate, onDestroy, onMount } from 'svelte';
17
16
  import { get } from 'svelte/store';
18
- import type { z } from 'zod';
19
-
20
- import { interpolate } from '$core/_utilities/i18n.utilities';
21
- import T from '$components/_utilities/locale-strings.svelte';
22
- import Alert from '$components/primitives/alert/alert.svelte';
23
- import Button from '$components/primitives/button/button.svelte';
24
- import Form from '$components/primitives/form/form.svelte';
25
- import { convertStringToKey } from '$journey/stages/_utilities/step.utilities';
26
- import { captureLinks } from '$journey/stages/_utilities/stage.utilities';
27
- import { styleStore } from '$core/style.store';
28
- import type { styleSchema } from '$core/style.store';
29
- import CallbackMapper from '$journey/_utilities/callback-mapper.svelte';
17
+
18
+ import {
19
+ Alert,
20
+ Button,
21
+ CallbackMapper,
22
+ captureLinks,
23
+ convertStringToKey,
24
+ Form,
25
+ interpolate,
26
+ styleStore,
27
+ T,
28
+ } from '$login-framework';
29
+
30
+ import type { JourneyStep } from '@forgerock/journey-client/types';
30
31
 
31
32
  import type {
32
33
  CallbackMetadata,
34
+ Maybe,
33
35
  StageFormObject,
34
36
  StageJourneyObject,
35
37
  StepMetadata,
36
- } from '$journey/journey.interfaces';
37
- import type { Maybe } from '$core/interfaces';
38
+ StyleObject,
39
+ } from '$login-framework';
38
40
 
39
41
  /** Display mode — determines which chrome is visible (header, links, etc.). */
40
42
  export let componentStyle: 'app' | 'inline' | 'modal';
@@ -51,11 +53,11 @@ submit button, links).
51
53
  /** Step + callback metadata from AM — policies, derived helpers, stage header. */
52
54
  export let metadata: Maybe<{ callbacks: CallbackMetadata[]; step: StepMetadata }>;
53
55
 
54
- /** The raw FRStep from the JavaScript SDK — contains all callbacks for this step. */
55
- export let step: FRStep;
56
+ /** The raw JourneyStep from Journey Client — contains all callbacks for this step. */
57
+ export let step: JourneyStep;
56
58
 
57
59
  // Subscribe to style store so the template can pass styles to child callbacks.
58
- let currentStyle: z.infer<typeof styleSchema> = get(styleStore);
60
+ let currentStyle: StyleObject = get(styleStore);
59
61
  const unsubStyle = styleStore.subscribe((v) => (currentStyle = v));
60
62
  onDestroy(unsubStyle);
61
63
 
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { format__COMPONENT_NAME_PASCAL__Label } from './__COMPONENT_SLUG__.utilities.js';
4
+
5
+ describe('__COMPONENT_NAME__ utilities', () => {
6
+ it('trims whitespace from label', () => {
7
+ expect(format__COMPONENT_NAME_PASCAL__Label(' hello ')).toBe('hello');
8
+ });
9
+ });
@@ -8,6 +8,6 @@
8
8
  /**
9
9
  * Example utility — replace with your own helpers.
10
10
  */
11
- export function format__COMPONENT_NAME__Label(value: string): string {
11
+ export function format__COMPONENT_NAME_PASCAL__Label(value: string): string {
12
12
  return value.trim();
13
13
  }
@@ -0,0 +1,18 @@
1
+ {
2
+ "_comment": "IDE-only tsconfig. See companion file in this directory for details.",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "allowJs": true,
12
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
13
+ "paths": {
14
+ "$login-framework": ["../../../experimental/custom/login-framework.ts"]
15
+ }
16
+ },
17
+ "include": ["**/*.ts", "**/*.svelte"]
18
+ }
@@ -0,0 +1,11 @@
1
+ /** Converts all backslashes to forward slashes for cross-platform path comparisons. */
2
+ export declare function normalizeSeparators(p: string): string;
3
+ /**
4
+ * Converts an arbitrary string to PascalCase, safe for use as a TypeScript identifier.
5
+ * Handles kebab-case, spaces, and preserves existing word boundaries in PascalCase input.
6
+ * Examples: "my-login-stage" → "MyLoginStage", "My Login Stage" → "MyLoginStage", "DefaultLogin" → "DefaultLogin"
7
+ *
8
+ * Intentionally duplicated from core/journey/_utilities/registry/registry.ts — tools/cli cannot
9
+ * depend on core/ (build-time vs. runtime boundary), so each package owns its own copy.
10
+ */
11
+ export declare function toPascalCase(str: string): string;
@@ -0,0 +1,17 @@
1
+ /** Converts all backslashes to forward slashes for cross-platform path comparisons. */
2
+ export function normalizeSeparators(p) {
3
+ return p.replace(/\\/g, '/');
4
+ }
5
+ /**
6
+ * Converts an arbitrary string to PascalCase, safe for use as a TypeScript identifier.
7
+ * Handles kebab-case, spaces, and preserves existing word boundaries in PascalCase input.
8
+ * Examples: "my-login-stage" → "MyLoginStage", "My Login Stage" → "MyLoginStage", "DefaultLogin" → "DefaultLogin"
9
+ *
10
+ * Intentionally duplicated from core/journey/_utilities/registry/registry.ts — tools/cli cannot
11
+ * depend on core/ (build-time vs. runtime boundary), so each package owns its own copy.
12
+ */
13
+ export function toPascalCase(str) {
14
+ return str
15
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
16
+ .replace(/^(.)/, (_, chr) => chr.toUpperCase());
17
+ }