@forgerock/login-framework-cli 0.0.0-beta-20260429204418

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/scripts/generate-registry.d.ts +2 -0
  4. package/dist/scripts/generate-registry.js +20 -0
  5. package/dist/src/commands/generate.d.ts +28 -0
  6. package/dist/src/commands/generate.js +137 -0
  7. package/dist/src/commands/init.d.ts +8 -0
  8. package/dist/src/commands/init.js +95 -0
  9. package/dist/src/commands/releases.d.ts +3 -0
  10. package/dist/src/commands/releases.js +12 -0
  11. package/dist/src/commands/source.d.ts +22 -0
  12. package/dist/src/commands/source.js +24 -0
  13. package/dist/src/commands/update.d.ts +5 -0
  14. package/dist/src/commands/update.js +36 -0
  15. package/dist/src/config/exclusions.d.ts +1 -0
  16. package/dist/src/config/exclusions.js +62 -0
  17. package/dist/src/config/version.d.ts +26 -0
  18. package/dist/src/config/version.js +41 -0
  19. package/dist/src/errors.d.ts +90 -0
  20. package/dist/src/errors.js +25 -0
  21. package/dist/src/main.d.ts +2 -0
  22. package/dist/src/main.js +34 -0
  23. package/dist/src/services/file-system.d.ts +13 -0
  24. package/dist/src/services/file-system.js +64 -0
  25. package/dist/src/services/registry.d.ts +22 -0
  26. package/dist/src/services/registry.js +169 -0
  27. package/dist/src/services/release.d.ts +27 -0
  28. package/dist/src/services/release.js +86 -0
  29. package/dist/src/templates/callback/__COMPONENT_SLUG__.svelte +73 -0
  30. package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +9 -0
  31. package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.ts +13 -0
  32. package/dist/src/templates/stage/__COMPONENT_SLUG__.svelte +148 -0
  33. package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +9 -0
  34. package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.ts +13 -0
  35. package/dist/src/utils.d.ts +2 -0
  36. package/dist/src/utils.js +4 -0
  37. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.d.ts +10 -0
  38. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.js +12 -0
  39. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.d.ts +1 -0
  40. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.js +7 -0
  41. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.d.ts +10 -0
  42. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.js +12 -0
  43. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.d.ts +1 -0
  44. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.js +7 -0
  45. package/package.json +59 -0
@@ -0,0 +1,62 @@
1
+ const EXCLUDED_PREFIXES = [
2
+ '.git/',
3
+ '.github/',
4
+ '.changeset/',
5
+ '.claude/',
6
+ '.husky/',
7
+ '.vscode/',
8
+ '.playwright-mcp/',
9
+ '.svelte-kit/',
10
+ 'node_modules/',
11
+ 'storybook-static/',
12
+ 'packages/login-widget/dist/',
13
+ 'packages/login-widget/svelte-package/',
14
+ 'packages/login-widget/node_modules/',
15
+ 'apps/login-app/build/',
16
+ 'apps/login-app/.svelte-kit/',
17
+ 'apps/login-app/node_modules/',
18
+ 'e2e/node_modules/',
19
+ 'e2e/test-results/',
20
+ 'e2e/playwright-report/',
21
+ 'e2e/blob-report/',
22
+ 'docs/',
23
+ 'specs/',
24
+ 'tools/',
25
+ ];
26
+ const EXCLUDED_FILES = [
27
+ '.env',
28
+ '.mcp.json',
29
+ 'pnpm-lock.yaml',
30
+ 'package-lock.json',
31
+ 'yarn.lock',
32
+ 'CLAUDE.md',
33
+ '.generator-version',
34
+ 'pnpm-workspace.yaml',
35
+ 'tsconfig.json',
36
+ 'bashrc',
37
+ 'CONTRIBUTING.md',
38
+ 'TESTING.md',
39
+ 'seed.spec.ts',
40
+ 'docker-compose.yml',
41
+ 'Dockerfile',
42
+ 'release.tar.gz',
43
+ ];
44
+ const EXCLUDED_PATTERNS = [/^\.env\..*/, /\.d\.ts$/];
45
+ const ALLOW_LIST = ['.env.example', '.env.docker.example'];
46
+ import { normalizeSeparators } from '../utils.js';
47
+ export function isExcluded(relativePath) {
48
+ const normalized = normalizeSeparators(relativePath);
49
+ if (ALLOW_LIST.some((allowed) => normalized === allowed)) {
50
+ return false;
51
+ }
52
+ if (EXCLUDED_FILES.some((file) => normalized === file)) {
53
+ return true;
54
+ }
55
+ if (EXCLUDED_PREFIXES.some((prefix) => `${normalized}/`.startsWith(prefix))) {
56
+ return true;
57
+ }
58
+ if (EXCLUDED_PATTERNS.some((pattern) => pattern.test(normalized))) {
59
+ return true;
60
+ }
61
+ return false;
62
+ }
@@ -0,0 +1,26 @@
1
+ import { Effect, Schema } from 'effect';
2
+ import { FileSystem } from '@effect/platform';
3
+ import { GeneratorVersionError } from '../errors.js';
4
+ declare const GeneratorVersionSchema: Schema.Struct<{
5
+ version: typeof Schema.String;
6
+ commitHash: typeof Schema.String;
7
+ generatedAt: typeof Schema.String;
8
+ }>;
9
+ export type GeneratorVersion = typeof GeneratorVersionSchema.Type;
10
+ export declare const readVersion: (directory: string) => Effect.Effect<{
11
+ readonly version: string;
12
+ readonly commitHash: string;
13
+ readonly generatedAt: string;
14
+ }, GeneratorVersionError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
15
+ export declare const writeVersion: (directory: string, version: GeneratorVersion) => Effect.Effect<void, import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
16
+ /**
17
+ * Asserts the given directory is a valid ping-lf project root by checking
18
+ * for a well-formed `.generator-version` file. Fails with a user-facing
19
+ * error message when the check does not pass.
20
+ */
21
+ export declare const assertValidProject: (directory: string) => Effect.Effect<{
22
+ readonly version: string;
23
+ readonly commitHash: string;
24
+ readonly generatedAt: string;
25
+ }, GeneratorVersionError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
26
+ export {};
@@ -0,0 +1,41 @@
1
+ import { Console, Effect, Schema } from 'effect';
2
+ import { FileSystem } from '@effect/platform';
3
+ import { GeneratorVersionError } from '../errors.js';
4
+ const GeneratorVersionSchema = Schema.Struct({
5
+ version: Schema.String,
6
+ commitHash: Schema.String,
7
+ generatedAt: Schema.String,
8
+ });
9
+ const VERSION_FILE = '.generator-version';
10
+ const decodeGeneratorVersion = Schema.decodeUnknown(Schema.parseJson(GeneratorVersionSchema));
11
+ const encodeGeneratorVersion = Schema.encode(Schema.parseJson(GeneratorVersionSchema));
12
+ const parseGeneratorFromFile = (filePath) => FileSystem.FileSystem.pipe(Effect.flatMap((fs) => fs.readFileString(filePath)), Effect.flatMap(decodeGeneratorVersion), Effect.mapError(() => new GeneratorVersionError({
13
+ message: 'Malformed .generator-version file',
14
+ path: filePath,
15
+ })));
16
+ export const readVersion = (directory) => Effect.gen(function* () {
17
+ const fs = yield* FileSystem.FileSystem;
18
+ const filePath = `${directory}/${VERSION_FILE}`;
19
+ const exists = yield* fs.exists(filePath);
20
+ if (!exists) {
21
+ return yield* new GeneratorVersionError({
22
+ message: 'No .generator-version file found. Is this a generated project?',
23
+ path: filePath,
24
+ });
25
+ }
26
+ return yield* parseGeneratorFromFile(filePath);
27
+ });
28
+ export const writeVersion = (directory, version) => Effect.gen(function* () {
29
+ const fs = yield* FileSystem.FileSystem;
30
+ const filePath = `${directory}/${VERSION_FILE}`;
31
+ const encoded = yield* encodeGeneratorVersion(version).pipe(Effect.orDie);
32
+ yield* fs.writeFileString(filePath, encoded + '\n');
33
+ });
34
+ /**
35
+ * Asserts the given directory is a valid ping-lf project root by checking
36
+ * for a well-formed `.generator-version` file. Fails with a user-facing
37
+ * error message when the check does not pass.
38
+ */
39
+ export const assertValidProject = (directory) => readVersion(directory).pipe(Effect.tapError((err) => Console.error(`\nError: ${err.message}\n` +
40
+ ` Make sure you are running this command from the root of an\n` +
41
+ ` initialized project (one created with "ping-lf init").\n`)));
@@ -0,0 +1,90 @@
1
+ declare const InvalidVersionError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
2
+ readonly _tag: "InvalidVersionError";
3
+ } & Readonly<A>;
4
+ export declare class InvalidVersionError extends InvalidVersionError_base<{
5
+ readonly version: string;
6
+ }> {
7
+ }
8
+ declare const ReleaseNetworkError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
9
+ readonly _tag: "ReleaseNetworkError";
10
+ } & Readonly<A>;
11
+ export declare class ReleaseNetworkError extends ReleaseNetworkError_base<{
12
+ readonly cause: string;
13
+ }> {
14
+ }
15
+ declare const ReleaseParseError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
16
+ readonly _tag: "ReleaseParseError";
17
+ } & Readonly<A>;
18
+ export declare class ReleaseParseError extends ReleaseParseError_base<{
19
+ readonly cause: string;
20
+ }> {
21
+ }
22
+ declare const ReleaseFsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
23
+ readonly _tag: "ReleaseFsError";
24
+ } & Readonly<A>;
25
+ export declare class ReleaseFsError extends ReleaseFsError_base<{
26
+ readonly operation: string;
27
+ readonly cause: string;
28
+ }> {
29
+ }
30
+ declare const ReleaseNotFoundError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
31
+ readonly _tag: "ReleaseNotFoundError";
32
+ } & Readonly<A>;
33
+ export declare class ReleaseNotFoundError extends ReleaseNotFoundError_base<{
34
+ readonly cause?: string;
35
+ }> {
36
+ }
37
+ declare const FileSystemError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
38
+ readonly _tag: "FileSystemError";
39
+ } & Readonly<A>;
40
+ export declare class FileSystemError extends FileSystemError_base<{
41
+ readonly operation: string;
42
+ readonly path: string;
43
+ readonly cause?: unknown;
44
+ }> {
45
+ }
46
+ declare const GeneratorVersionError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
47
+ readonly _tag: "GeneratorVersionError";
48
+ } & Readonly<A>;
49
+ export declare class GeneratorVersionError extends GeneratorVersionError_base<{
50
+ readonly message: string;
51
+ readonly path?: string;
52
+ }> {
53
+ }
54
+ declare const RegistryScanError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
55
+ readonly _tag: "RegistryScanError";
56
+ } & Readonly<A>;
57
+ export declare class RegistryScanError extends RegistryScanError_base<{
58
+ readonly directory: string;
59
+ readonly cause?: unknown;
60
+ }> {
61
+ }
62
+ declare const DirectoryConflictError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
63
+ readonly _tag: "DirectoryConflictError";
64
+ } & Readonly<A>;
65
+ export declare class DirectoryConflictError extends DirectoryConflictError_base<{
66
+ readonly path: string;
67
+ }> {
68
+ }
69
+ declare const DirectoryNotEmptyError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
70
+ readonly _tag: "DirectoryNotEmptyError";
71
+ } & Readonly<A>;
72
+ export declare class DirectoryNotEmptyError extends DirectoryNotEmptyError_base<{
73
+ readonly path: string;
74
+ }> {
75
+ }
76
+ declare const InvalidComponentNameError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
77
+ readonly _tag: "InvalidComponentNameError";
78
+ } & Readonly<A>;
79
+ export declare class InvalidComponentNameError extends InvalidComponentNameError_base<{
80
+ readonly name: string;
81
+ }> {
82
+ }
83
+ declare const ComponentAlreadyExistsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
84
+ readonly _tag: "ComponentAlreadyExistsError";
85
+ } & Readonly<A>;
86
+ export declare class ComponentAlreadyExistsError extends ComponentAlreadyExistsError_base<{
87
+ readonly path: string;
88
+ }> {
89
+ }
90
+ export {};
@@ -0,0 +1,25 @@
1
+ import { Data } from 'effect';
2
+ export class InvalidVersionError extends Data.TaggedError('InvalidVersionError') {
3
+ }
4
+ export class ReleaseNetworkError extends Data.TaggedError('ReleaseNetworkError') {
5
+ }
6
+ export class ReleaseParseError extends Data.TaggedError('ReleaseParseError') {
7
+ }
8
+ export class ReleaseFsError extends Data.TaggedError('ReleaseFsError') {
9
+ }
10
+ export class ReleaseNotFoundError extends Data.TaggedError('ReleaseNotFoundError') {
11
+ }
12
+ export class FileSystemError extends Data.TaggedError('FileSystemError') {
13
+ }
14
+ export class GeneratorVersionError extends Data.TaggedError('GeneratorVersionError') {
15
+ }
16
+ export class RegistryScanError extends Data.TaggedError('RegistryScanError') {
17
+ }
18
+ export class DirectoryConflictError extends Data.TaggedError('DirectoryConflictError') {
19
+ }
20
+ export class DirectoryNotEmptyError extends Data.TaggedError('DirectoryNotEmptyError') {
21
+ }
22
+ export class InvalidComponentNameError extends Data.TaggedError('InvalidComponentNameError') {
23
+ }
24
+ export class ComponentAlreadyExistsError extends Data.TaggedError('ComponentAlreadyExistsError') {
25
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from '@effect/cli';
3
+ import { NodeContext, NodeRuntime } from '@effect/platform-node';
4
+ import { Console, Effect } from 'effect';
5
+ import { createRequire } from 'node:module';
6
+ import { initCommand } from './commands/init.js';
7
+ import { generateCommand } from './commands/generate.js';
8
+ import { releasesCommand } from './commands/releases.js';
9
+ import { updateCommand } from './commands/update.js';
10
+ import { GithubReleaseLayer } from './services/release.js';
11
+ // Read the version from package.json at runtime so it stays in sync
12
+ // with the published package version after changesets bumps it.
13
+ const { version } = createRequire(import.meta.url)('../package.json');
14
+ const rootCommand = Command.make('ping-lf').pipe(Command.withDescription('CLI for initializing, scaffolding, and updating Ping Login Widget and Login App custom component projects.'), Command.withSubcommands([initCommand, generateCommand, updateCommand, releasesCommand]));
15
+ const cli = Command.run(rootCommand, {
16
+ name: 'ping-lf',
17
+ version,
18
+ });
19
+ cli(process.argv).pipe(Effect.catchTag('DirectoryConflictError', (err) => Console.error(`\nError: "${err.path}" already contains a framework project.` +
20
+ `\n Did you mean to use --local?\n` +
21
+ `\n ping-lf init <new-directory> --local ${err.path}\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('DirectoryNotEmptyError', (err) => Console.error(`\nError: "${err.path}" already exists and is not empty.` +
22
+ `\n Choose a different directory name, or delete the existing directory first.\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseNetworkError', (err) => Console.error(`\nError: Could not reach GitHub to download the release.\n` +
23
+ ` ${err.cause}\n\n` +
24
+ ` • Check your network connection and try again.\n` +
25
+ ` • Use a local path: ping-lf init <dir> --local <path>\n` +
26
+ ` • Specify a version: ping-lf init <dir> --version v1.0.0\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseParseError', (err) => Console.error(`\nError: Failed to parse the release data.\n ${err.cause}\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseFsError', (err) => Console.error(`\nError: Filesystem error during release download (${err.operation}).\n ${err.cause}\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('InvalidVersionError', (err) => Console.error(`\nError: "${err.version}" is not a valid version tag.\n` +
27
+ ` Expected semver format like v1.0.0.\n` +
28
+ ` Use "ping-lf releases" to list available versions.\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseNotFoundError', (err) => Console.error(`\nError: No releases found on GitHub.` +
29
+ (err.cause ? `\n ${err.cause}` : '') +
30
+ `\n\n • Check your network connection and try again.\n` +
31
+ ` • Use a local path: ping-lf init <dir> --local <path>\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('InvalidComponentNameError', (err) => Console.error(`\nError: "${err.name}" is not a valid component name.\n` +
32
+ ` Names must be PascalCase, start with an uppercase letter, and contain only letters and digits.\n` +
33
+ ` Examples: MyCallback, JWTLogin, DefaultStage\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ComponentAlreadyExistsError', (err) => Console.error(`\nError: component directory already exists: ${err.path}\n` +
34
+ ` Choose a different name or delete the existing directory first.\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.provide(GithubReleaseLayer), Effect.provide(NodeContext.layer), (effect) => NodeRuntime.runMain(effect, { disableErrorReporting: true }));
@@ -0,0 +1,13 @@
1
+ import { Effect } from 'effect';
2
+ import { FileSystem } from '@effect/platform';
3
+ import { FileSystemError } from '../errors.js';
4
+ /**
5
+ * Returns true if the given directory looks like an initialized framework
6
+ * project. Checks for `package.json` combined with either an `experimental/`
7
+ * subdirectory or a `pnpm-workspace.yaml` file — both are reliable indicators
8
+ * that the directory was created by `ping-lf init`.
9
+ */
10
+ export declare const isFrameworkDirectory: (dir: string) => Effect.Effect<boolean, never, FileSystem.FileSystem>;
11
+ /** Expand a leading `~` to the user's home directory. */
12
+ export declare function expandTilde(p: string): string;
13
+ export declare const copyWithExclusions: (sourceDir: string, targetDir: string) => Effect.Effect<void, FileSystemError, FileSystem.FileSystem>;
@@ -0,0 +1,64 @@
1
+ import { Effect } from 'effect';
2
+ import { FileSystem } from '@effect/platform';
3
+ import { isExcluded } from '../config/exclusions.js';
4
+ import { FileSystemError } from '../errors.js';
5
+ import path from 'node:path';
6
+ import { normalizeSeparators } from '../utils.js';
7
+ const wrapFsCall = (operation, fsPath) => (effect) => {
8
+ const fail = (cause) => Effect.fail(new FileSystemError({ operation, path: fsPath, cause }));
9
+ return effect.pipe(Effect.catchTags({ SystemError: fail, BadArgument: fail }));
10
+ };
11
+ /**
12
+ * Returns true if the given directory looks like an initialized framework
13
+ * project. Checks for `package.json` combined with either an `experimental/`
14
+ * subdirectory or a `pnpm-workspace.yaml` file — both are reliable indicators
15
+ * that the directory was created by `ping-lf init`.
16
+ */
17
+ export const isFrameworkDirectory = Effect.fnUntraced(function* (dir) {
18
+ const fs = yield* FileSystem.FileSystem;
19
+ const check = (p) => fs.exists(path.join(dir, p)).pipe(Effect.orElseSucceed(() => false));
20
+ const hasPkgJson = yield* check('package.json');
21
+ const hasExperimental = yield* check('experimental');
22
+ const hasPnpmWorkspace = yield* check('pnpm-workspace.yaml');
23
+ return hasPkgJson && (hasExperimental || hasPnpmWorkspace);
24
+ });
25
+ const PROTECTED_DIRS = ['experimental/custom/callbacks/', 'experimental/custom/stages/'];
26
+ /** Expand a leading `~` to the user's home directory. */
27
+ export function expandTilde(p) {
28
+ if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
29
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '~';
30
+ return p.replace('~', home);
31
+ }
32
+ return p;
33
+ }
34
+ function isProtected(relativePath) {
35
+ const normalized = normalizeSeparators(relativePath);
36
+ return PROTECTED_DIRS.some((dir) => `${normalized}/`.startsWith(dir));
37
+ }
38
+ const walk = (fs, sourceDir, targetDir, dir) => Effect.gen(function* () {
39
+ const entries = yield* fs.readDirectory(dir).pipe(wrapFsCall('readDirectory', dir));
40
+ yield* Effect.forEach(entries.filter((entry) => {
41
+ const relativePath = path.relative(sourceDir, path.join(dir, entry));
42
+ return !isExcluded(relativePath) && !isProtected(relativePath);
43
+ }), (entry) => Effect.gen(function* () {
44
+ const fullPath = path.join(dir, entry);
45
+ const relativePath = path.relative(sourceDir, fullPath);
46
+ const stat = yield* fs.stat(fullPath).pipe(wrapFsCall('stat', fullPath));
47
+ const targetPath = path.join(targetDir, relativePath);
48
+ yield* Effect.if(stat.type === 'Directory', {
49
+ onTrue: () => fs
50
+ .makeDirectory(targetPath, { recursive: true })
51
+ .pipe(wrapFsCall('makeDirectory', targetPath), Effect.andThen(walk(fs, sourceDir, targetDir, fullPath))),
52
+ onFalse: () => {
53
+ const parentDir = path.dirname(targetPath);
54
+ return fs
55
+ .makeDirectory(parentDir, { recursive: true })
56
+ .pipe(wrapFsCall('makeDirectory', parentDir), Effect.andThen(fs.copyFile(fullPath, targetPath).pipe(wrapFsCall('copyFile', fullPath))));
57
+ },
58
+ });
59
+ }), { concurrency: 'unbounded' });
60
+ });
61
+ export const copyWithExclusions = (sourceDir, targetDir) => Effect.gen(function* () {
62
+ const fs = yield* FileSystem.FileSystem;
63
+ yield* walk(fs, sourceDir, targetDir, sourceDir);
64
+ });
@@ -0,0 +1,22 @@
1
+ import { Effect } from 'effect';
2
+ import { FileSystem, Path } from '@effect/platform';
3
+ import { RegistryScanError } from '../errors.js';
4
+ type ComponentType = 'stage' | 'callback';
5
+ /** Extracts names of all `export let` prop declarations from a Svelte component's `<script>` block. */
6
+ export declare function parseAcceptedProps(content: string): string[];
7
+ /** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
8
+ export declare const parseComponentHeader: (filePath: string, content: string) => Effect.Effect<{
9
+ type: ComponentType;
10
+ name: string;
11
+ }, RegistryScanError>;
12
+ /**
13
+ * Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
14
+ * `@component`-annotated Svelte files and writes
15
+ * `core/journey/_utilities/custom-registry.ts`.
16
+ *
17
+ * All I/O runs in-process via the platform `FileSystem` service — no subprocess
18
+ * spawning. Validation errors across multiple components are collected and
19
+ * reported together.
20
+ */
21
+ export declare const runRegistryScript: (projectDir: string) => Effect.Effect<void, RegistryScanError, FileSystem.FileSystem | Path.Path>;
22
+ export {};
@@ -0,0 +1,169 @@
1
+ import { Console, Effect } from 'effect';
2
+ import { FileSystem, Path } from '@effect/platform';
3
+ import { RegistryScanError } from '../errors.js';
4
+ // --------------------------------------------------------------------------
5
+ // Helpers (exported for testing)
6
+ // --------------------------------------------------------------------------
7
+ /** Extracts names of all `export let` prop declarations from a Svelte component's `<script>` block. */
8
+ export function parseAcceptedProps(content) {
9
+ const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
10
+ if (!scriptMatch)
11
+ return [];
12
+ const props = [];
13
+ const regex = /export\s+let\s+(\w+)/g;
14
+ let m;
15
+ while ((m = regex.exec(scriptMatch[1])) !== null)
16
+ props.push(m[1]);
17
+ return props;
18
+ }
19
+ function toPascalCase(str) {
20
+ return str
21
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
22
+ .replace(/^(.)/, (_, chr) => chr.toUpperCase());
23
+ }
24
+ /** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
25
+ export const parseComponentHeader = (filePath, content) => {
26
+ const fail = (cause) => Effect.fail(new RegistryScanError({ directory: filePath, cause }));
27
+ const commentMatch = content.match(/^<!--([\s\S]*?)-->/);
28
+ if (!commentMatch)
29
+ return fail('Missing @component header. Every custom component must begin with:\n' +
30
+ '<!--\n @component\n Type: stage|callback\n Name: <ComponentName>\n -->');
31
+ const block = commentMatch[1];
32
+ if (!block.includes('@component'))
33
+ return fail('Missing "@component" tag in the opening comment.');
34
+ const typeMatch = block.match(/Type:\s*(\S+)/);
35
+ if (!typeMatch)
36
+ return fail('Missing "Type:" field in @component header. Expected: Type: stage or Type: callback');
37
+ const rawType = typeMatch[1].toLowerCase();
38
+ if (rawType !== 'stage' && rawType !== 'callback')
39
+ return fail(`Invalid Type value "${typeMatch[1]}". Must be "stage" or "callback".`);
40
+ const nameMatch = block.match(/Name:\s*(.+)/);
41
+ if (!nameMatch)
42
+ return fail('Missing "Name:" field in @component header. Expected: Name: <ComponentName>');
43
+ return Effect.succeed({ type: rawType, name: nameMatch[1].trim() });
44
+ };
45
+ // --------------------------------------------------------------------------
46
+ // File scanning
47
+ // --------------------------------------------------------------------------
48
+ const findSvelteFiles = (fs, path, dir) => fs.exists(dir).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((exists) => !exists
49
+ ? Effect.succeed([])
50
+ : fs.readDirectory(dir).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: dir, cause })), Effect.flatMap((entries) => Effect.forEach(entries, (entry) => {
51
+ const fullPath = path.join(dir, entry);
52
+ return fs.stat(fullPath).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: dir, cause })), Effect.flatMap((stat) => stat.type === 'Directory'
53
+ ? findSvelteFiles(fs, path, fullPath)
54
+ : Effect.succeed(entry.endsWith('.svelte') && !entry.endsWith('.story.svelte')
55
+ ? [fullPath]
56
+ : [])));
57
+ }, { concurrency: 'unbounded' })), Effect.map((nested) => nested.flat()))));
58
+ const scanDirectory = (fs, path, dir, expectedType) => findSvelteFiles(fs, path, dir).pipe(Effect.flatMap((files) => Effect.validateAll(files, (filePath) => fs.readFileString(filePath).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: filePath, cause })), Effect.flatMap((content) => parseComponentHeader(filePath, content).pipe(Effect.flatMap(({ type, name }) => type !== expectedType
59
+ ? Effect.fail(new RegistryScanError({
60
+ directory: filePath,
61
+ cause: `Component in /experimental/custom/${expectedType}s/ declares Type: "${type}". Must declare Type: ${expectedType}`,
62
+ }))
63
+ : Effect.succeed({
64
+ filePath,
65
+ name,
66
+ type,
67
+ acceptedProps: parseAcceptedProps(content),
68
+ })))))).pipe(Effect.mapError((errors) => new RegistryScanError({
69
+ directory: dir,
70
+ cause: errors.map((e) => String(e.cause)).join('\n'),
71
+ })))));
72
+ // --------------------------------------------------------------------------
73
+ // Registry content builder (pure)
74
+ // --------------------------------------------------------------------------
75
+ function buildRegistryContent(path, registryDir, stageComponents, callbackComponents) {
76
+ const toEntry = (prefix) => ({ filePath, name, acceptedProps }) => {
77
+ const relPath = path.relative(registryDir, filePath).replace(/\\/g, '/');
78
+ const importPath = relPath.startsWith('.') ? relPath : `./${relPath}`;
79
+ return { varName: `${prefix}${toPascalCase(name)}`, importPath, name, acceptedProps };
80
+ };
81
+ const stageEntries = stageComponents.map(toEntry('Stage'));
82
+ const callbackEntries = callbackComponents.map(toEntry('Callback'));
83
+ const lines = [
84
+ `/**`,
85
+ ` * AUTO-GENERATED — do not edit by hand.`,
86
+ ` * Run \`pnpm build:widget\` or \`pnpm --filter @forgerock/login-widget exec vite build\` to regenerate.`,
87
+ ` *`,
88
+ ` * Source: /experimental/custom/stages/ and /experimental/custom/callbacks/`,
89
+ ` */`,
90
+ ``,
91
+ `import type { Component } from 'svelte';`,
92
+ ``,
93
+ `export interface CustomRegistryEntry {`,
94
+ ` component: Component;`,
95
+ ` /** Props declared via \`export let\` in the component — only these are forwarded by the mapper. */`,
96
+ ` acceptedProps: string[];`,
97
+ `}`,
98
+ ``,
99
+ ];
100
+ if (stageEntries.length > 0) {
101
+ lines.push(`// Stage overrides / extensions`);
102
+ for (const { varName, importPath } of stageEntries)
103
+ lines.push(`import ${varName} from '${importPath}';`);
104
+ lines.push(``);
105
+ }
106
+ if (callbackEntries.length > 0) {
107
+ lines.push(`// Callback overrides / extensions`);
108
+ for (const { varName, importPath } of callbackEntries)
109
+ lines.push(`import ${varName} from '${importPath}';`);
110
+ lines.push(``);
111
+ }
112
+ lines.push(`export const customStageRegistry: Record<string, CustomRegistryEntry> = {`);
113
+ for (const { varName, name, acceptedProps } of stageEntries)
114
+ lines.push(` ${JSON.stringify(name)}: { component: ${varName}, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
115
+ lines.push(`};`);
116
+ lines.push(``);
117
+ lines.push(`export const customCallbackRegistry: Record<string, CustomRegistryEntry> = {`);
118
+ for (const { varName, name, acceptedProps } of callbackEntries)
119
+ lines.push(` ${JSON.stringify(name)}: { component: ${varName}, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
120
+ lines.push(`};`);
121
+ lines.push(``);
122
+ return lines.join('\n');
123
+ }
124
+ // --------------------------------------------------------------------------
125
+ // Public API
126
+ // --------------------------------------------------------------------------
127
+ /**
128
+ * Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
129
+ * `@component`-annotated Svelte files and writes
130
+ * `core/journey/_utilities/custom-registry.ts`.
131
+ *
132
+ * All I/O runs in-process via the platform `FileSystem` service — no subprocess
133
+ * spawning. Validation errors across multiple components are collected and
134
+ * reported together.
135
+ */
136
+ export const runRegistryScript = (projectDir) => Effect.gen(function* () {
137
+ const fs = yield* FileSystem.FileSystem;
138
+ const path = yield* Path.Path;
139
+ const stageDir = path.join(projectDir, 'experimental', 'custom', 'stages');
140
+ const callbackDir = path.join(projectDir, 'experimental', 'custom', 'callbacks');
141
+ const registryDir = path.join(projectDir, 'core', 'journey', '_utilities');
142
+ const registryPath = path.join(registryDir, 'custom-registry.ts');
143
+ const [stageComponents, callbackComponents] = yield* Effect.all([
144
+ scanDirectory(fs, path, stageDir, 'stage'),
145
+ scanDirectory(fs, path, callbackDir, 'callback'),
146
+ ], { concurrency: 'unbounded' });
147
+ const content = buildRegistryContent(path, registryDir, stageComponents, callbackComponents);
148
+ yield* fs
149
+ .makeDirectory(registryDir, { recursive: true })
150
+ .pipe(Effect.mapError((cause) => new RegistryScanError({ directory: registryDir, cause })));
151
+ yield* fs
152
+ .writeFileString(registryPath, content)
153
+ .pipe(Effect.mapError((cause) => new RegistryScanError({ directory: registryPath, cause })));
154
+ const total = stageComponents.length + callbackComponents.length;
155
+ if (total === 0) {
156
+ yield* Console.log(`custom-registry.ts generated (no custom components found — registries are empty)`);
157
+ }
158
+ else {
159
+ yield* Console.log(`custom-registry.ts generated:`);
160
+ if (stageComponents.length > 0)
161
+ yield* Console.log(` Stages (${stageComponents.length}): ${stageComponents
162
+ .map((c) => c.name)
163
+ .join(', ')}`);
164
+ if (callbackComponents.length > 0)
165
+ yield* Console.log(` Callbacks (${callbackComponents.length}): ${callbackComponents
166
+ .map((c) => c.name)
167
+ .join(', ')}`);
168
+ }
169
+ });
@@ -0,0 +1,27 @@
1
+ import { Context, Effect, Layer, Scope } from 'effect';
2
+ import { FileSystem, HttpClient } from '@effect/platform';
3
+ import { InvalidVersionError, ReleaseFsError, ReleaseNetworkError, ReleaseNotFoundError, ReleaseParseError } from '../errors.js';
4
+ export declare function validateVersion(version: string): Effect.Effect<string, InvalidVersionError>;
5
+ /**
6
+ * Parses a GitHub releases API JSON response and returns an array of
7
+ * `{ tag, publishedAt }` objects, excluding drafts and pre-releases.
8
+ */
9
+ export declare function parseReleaseTags(json: string): Effect.Effect<Array<{
10
+ tag: string;
11
+ publishedAt: string;
12
+ }>, ReleaseParseError>;
13
+ declare const Release_base: Context.TagClass<Release, "Release", {
14
+ readonly archiveUrl: (version: string) => string;
15
+ readonly resolveLatest: () => Effect.Effect<string, ReleaseNetworkError | ReleaseParseError>;
16
+ readonly listReleases: () => Effect.Effect<Array<{
17
+ tag: string;
18
+ publishedAt: string;
19
+ }>, ReleaseNetworkError | ReleaseParseError | ReleaseNotFoundError>;
20
+ readonly fetch: (version: string, targetDir: string) => Effect.Effect<string, InvalidVersionError | ReleaseFsError | ReleaseNetworkError | ReleaseParseError, FileSystem.FileSystem | Scope.Scope>;
21
+ readonly fetchBranch: (branch: string, targetDir: string) => Effect.Effect<string, ReleaseFsError | ReleaseNetworkError | ReleaseParseError, FileSystem.FileSystem | Scope.Scope>;
22
+ }>;
23
+ export declare class Release extends Release_base {
24
+ }
25
+ export declare const makeGithubReleaseLayer: (httpLayer: Layer.Layer<HttpClient.HttpClient>) => Layer.Layer<Release, never, never>;
26
+ export declare const GithubReleaseLayer: Layer.Layer<Release, never, never>;
27
+ export {};