@forgerock/login-framework-cli 0.0.0-beta.0 → 0.1.1
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/README.md +107 -34
- package/dist/{commands → src/commands}/generate.d.ts +3 -2
- package/dist/{commands → src/commands}/generate.js +15 -8
- package/dist/src/commands/init.d.ts +16 -0
- package/dist/{commands → src/commands}/init.js +8 -10
- package/dist/{commands → src/commands}/source.d.ts +1 -1
- package/dist/{commands → src/commands}/update.d.ts +4 -2
- package/dist/{commands → src/commands}/update.js +9 -8
- package/dist/{config → src/config}/version.d.ts +1 -4
- package/dist/{config → src/config}/version.js +1 -2
- package/dist/src/main.js +61 -0
- package/dist/src/mcp.d.ts +52 -0
- package/dist/src/mcp.js +182 -0
- package/dist/{services → src/services}/registry.d.ts +9 -2
- package/dist/{services → src/services}/registry.js +20 -13
- package/dist/{services → src/services}/release.d.ts +2 -1
- package/dist/{services → src/services}/release.js +2 -2
- package/dist/src/templates/callback/__COMPONENT_SLUG__.mock.ts +22 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.stories.js +28 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.story.svelte +59 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.svelte +49 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +9 -0
- package/dist/{templates → src/templates}/callback/__COMPONENT_SLUG__.utilities.ts +1 -1
- package/dist/src/templates/stage/__COMPONENT_SLUG__.mock.ts +23 -0
- package/dist/src/templates/stage/__COMPONENT_SLUG__.stories.js +43 -0
- package/dist/src/templates/stage/__COMPONENT_SLUG__.story.svelte +47 -0
- package/dist/{templates → src/templates}/stage/__COMPONENT_SLUG__.svelte +21 -19
- package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +9 -0
- package/dist/{templates → src/templates}/stage/__COMPONENT_SLUG__.utilities.ts +1 -1
- package/dist/src/templates/tsconfig.json +18 -0
- package/dist/src/utils.d.ts +11 -0
- package/dist/src/utils.js +17 -0
- package/package.json +13 -10
- package/dist/commands/init.d.ts +0 -8
- package/dist/main.js +0 -34
- package/dist/templates/callback/__COMPONENT_SLUG__.svelte +0 -73
- package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +0 -9
- package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +0 -9
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -4
- package/scripts/generate-registry.mjs +0 -23
- package/dist/{commands → src/commands}/releases.d.ts +0 -0
- package/dist/{commands → src/commands}/releases.js +0 -0
- package/dist/{commands → src/commands}/source.js +0 -0
- package/dist/{config → src/config}/exclusions.d.ts +0 -0
- package/dist/{config → src/config}/exclusions.js +0 -0
- package/dist/{errors.d.ts → src/errors.d.ts} +8 -8
- package/dist/{errors.js → src/errors.js} +2 -2
- package/dist/{main.d.ts → src/main.d.ts} +0 -0
- package/dist/{services → src/services}/file-system.d.ts +1 -1
- package/dist/{services → src/services}/file-system.js +2 -2
package/dist/src/mcp.js
ADDED
|
@@ -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 a Git tag like @forgerock/login-widget@2.1.0 or 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 Git release tag to download (e.g. @forgerock/login-widget@2.1.0 or v1.2.0). Defaults to main.',
|
|
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 Git release tag to update to (e.g. @forgerock/login-widget@2.1.0 or v1.2.0). Defaults to main.',
|
|
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,14 +1,21 @@
|
|
|
1
|
-
import { Effect } from 'effect';
|
|
2
1
|
import { FileSystem, Path } from '@effect/platform';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
3
|
import { RegistryScanError } from '../errors.js';
|
|
4
4
|
type ComponentType = 'stage' | 'callback';
|
|
5
|
-
|
|
5
|
+
interface ComponentEntry {
|
|
6
|
+
filePath: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: ComponentType;
|
|
9
|
+
acceptedProps: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Extracts names of all `export let` prop declarations from a Svelte component's instance script. */
|
|
6
12
|
export declare function parseAcceptedProps(content: string): string[];
|
|
7
13
|
/** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
|
|
8
14
|
export declare const parseComponentHeader: (filePath: string, content: string) => Effect.Effect<{
|
|
9
15
|
type: ComponentType;
|
|
10
16
|
name: string;
|
|
11
17
|
}, RegistryScanError>;
|
|
18
|
+
export declare function buildRegistryContent(path: Path.Path, registryDir: string, stageComponents: ComponentEntry[], callbackComponents: ComponentEntry[]): string;
|
|
12
19
|
/**
|
|
13
20
|
* Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
|
|
14
21
|
* `@component`-annotated Svelte files and writes
|
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import { Console, Effect } from 'effect';
|
|
2
1
|
import { FileSystem, Path } from '@effect/platform';
|
|
2
|
+
import { Console, Effect } from 'effect';
|
|
3
|
+
import { parse } from 'svelte/compiler';
|
|
3
4
|
import { RegistryScanError } from '../errors.js';
|
|
4
5
|
// --------------------------------------------------------------------------
|
|
5
6
|
// Helpers (exported for testing)
|
|
6
7
|
// --------------------------------------------------------------------------
|
|
7
|
-
/** Extracts names of all `export let` prop declarations from a Svelte component's
|
|
8
|
+
/** Extracts names of all `export let` prop declarations from a Svelte component's instance script. */
|
|
8
9
|
export function parseAcceptedProps(content) {
|
|
9
|
-
const
|
|
10
|
-
if (!scriptMatch)
|
|
11
|
-
return [];
|
|
10
|
+
const abstractSyntaxTree = parse(content, { modern: false });
|
|
12
11
|
const props = [];
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
for (const node of abstractSyntaxTree.instance?.content.body ?? []) {
|
|
13
|
+
if (node.type !== 'ExportNamedDeclaration') {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (node.declaration?.type === 'VariableDeclaration' && node.declaration.kind === 'let') {
|
|
17
|
+
for (const declarator of node.declaration.declarations) {
|
|
18
|
+
if (declarator.id.type === 'Identifier') {
|
|
19
|
+
props.push(declarator.id.name);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
17
24
|
return props;
|
|
18
25
|
}
|
|
19
26
|
function toPascalCase(str) {
|
|
@@ -72,7 +79,7 @@ const scanDirectory = (fs, path, dir, expectedType) => findSvelteFiles(fs, path,
|
|
|
72
79
|
// --------------------------------------------------------------------------
|
|
73
80
|
// Registry content builder (pure)
|
|
74
81
|
// --------------------------------------------------------------------------
|
|
75
|
-
function buildRegistryContent(path, registryDir, stageComponents, callbackComponents) {
|
|
82
|
+
export function buildRegistryContent(path, registryDir, stageComponents, callbackComponents) {
|
|
76
83
|
const toEntry = (prefix) => ({ filePath, name, acceptedProps }) => {
|
|
77
84
|
const relPath = path.relative(registryDir, filePath).replace(/\\/g, '/');
|
|
78
85
|
const importPath = relPath.startsWith('.') ? relPath : `./${relPath}`;
|
|
@@ -111,12 +118,12 @@ function buildRegistryContent(path, registryDir, stageComponents, callbackCompon
|
|
|
111
118
|
}
|
|
112
119
|
lines.push(`export const customStageRegistry: Record<string, CustomRegistryEntry> = {`);
|
|
113
120
|
for (const { varName, name, acceptedProps } of stageEntries)
|
|
114
|
-
lines.push(` ${JSON.stringify(name)}: { component
|
|
121
|
+
lines.push(` ${JSON.stringify(name)}: { get component() { return ${varName}; }, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
|
|
115
122
|
lines.push(`};`);
|
|
116
123
|
lines.push(``);
|
|
117
124
|
lines.push(`export const customCallbackRegistry: Record<string, CustomRegistryEntry> = {`);
|
|
118
125
|
for (const { varName, name, acceptedProps } of callbackEntries)
|
|
119
|
-
lines.push(` ${JSON.stringify(name)}: { component
|
|
126
|
+
lines.push(` ${JSON.stringify(name)}: { get component() { return ${varName}; }, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
|
|
120
127
|
lines.push(`};`);
|
|
121
128
|
lines.push(``);
|
|
122
129
|
return lines.join('\n');
|
|
@@ -138,7 +145,7 @@ export const runRegistryScript = (projectDir) => Effect.gen(function* () {
|
|
|
138
145
|
const path = yield* Path.Path;
|
|
139
146
|
const stageDir = path.join(projectDir, 'experimental', 'custom', 'stages');
|
|
140
147
|
const callbackDir = path.join(projectDir, 'experimental', 'custom', 'callbacks');
|
|
141
|
-
const registryDir = path.join(projectDir, 'core', 'journey', '_utilities');
|
|
148
|
+
const registryDir = path.join(projectDir, 'core', 'journey', '_utilities', 'registry');
|
|
142
149
|
const registryPath = path.join(registryDir, 'custom-registry.ts');
|
|
143
150
|
const [stageComponents, callbackComponents] = yield* Effect.all([
|
|
144
151
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Schema } from 'effect';
|
|
2
1
|
import { FileSystem, HttpClient, HttpClientResponse } from '@effect/platform';
|
|
3
2
|
import { NodeHttpClient } from '@effect/platform-node';
|
|
3
|
+
import { Context, Effect, Layer, Schema } from 'effect';
|
|
4
4
|
import { extract } from 'tar';
|
|
5
5
|
import { InvalidVersionError, ReleaseFsError, ReleaseNetworkError, ReleaseNotFoundError, ReleaseParseError, } from '../errors.js';
|
|
6
6
|
const REPO = 'ForgeRock/forgerock-web-login-framework';
|
|
@@ -8,7 +8,7 @@ const API_HEADERS = {
|
|
|
8
8
|
'User-Agent': 'ping-lf-cli',
|
|
9
9
|
Accept: 'application/vnd.github.v3+json',
|
|
10
10
|
};
|
|
11
|
-
const VERSION_REGEX = /^v?\d+\.\d+\.\d+(-[\w.]+)
|
|
11
|
+
const VERSION_REGEX = /^(?:v?\d+\.\d+\.\d+(-[\w.]+)?|@forgerock\/login-widget@\d+\.\d+\.\d+(-[\w.]+)?)$/;
|
|
12
12
|
const toCauseString = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
13
13
|
export function validateVersion(version) {
|
|
14
14
|
return VERSION_REGEX.test(version)
|
|
@@ -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
|
+
});
|
|
@@ -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>
|