@forgerock/login-framework-cli 0.0.0-beta-20261013223421 → 0.0.0-beta-20261021173425

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 CHANGED
@@ -45,11 +45,11 @@ pnpm dev
45
45
  1. Downloads the framework archive from GitHub Releases (or copies from `--local`)
46
46
  2. Copies framework files, excluding build artifacts, secrets, and `tools/`
47
47
  3. Writes a minimal `pnpm-workspace.yaml`
48
- 4. Creates `experimental/custom/callbacks/` and `experimental/custom/stages/` with `.gitkeep` placeholders
48
+ 4. Creates `experimental/custom/callbacks/` and `experimental/custom/stages/` with `.gitkeep` placeholders, and places the page header and footer wrapper files `experimental/custom/header/header.svelte` and `experimental/custom/footer/footer.svelte`
49
49
  5. Copies the component authoring guide to `experimental/custom/README.md`
50
50
  6. Writes `.generator-version` to track the framework version in use
51
51
 
52
- The `core/journey/_utilities/registry/custom-registry.ts` file is generated by the framework's Vite plugin on the first `pnpm dev` or `pnpm build:widget` — the CLI never touches it.
52
+ The `core/journey/_utilities/registry/custom-registry.ts` file is generated by the framework's Vite plugin on the first `pnpm dev` or `pnpm build:widget` — the CLI never generates it. (With `--local`, the bulk copy carries over whatever `custom-registry.ts` exists in the local source; the Vite plugin regenerates it on the next build.)
53
53
 
54
54
  ---
55
55
 
@@ -93,6 +93,8 @@ Name: NameCallback
93
93
 
94
94
  Setting `Name` to an existing default (e.g. `NameCallback`, `DefaultLogin`) **overrides** that component. Setting it to a new name **extends** the framework with a custom type.
95
95
 
96
+ Page headers and footers are not scaffolded with `generate`: they are fixed wrapper files (`experimental/custom/header/header.svelte` and `experimental/custom/footer/footer.svelte`), generated on first build by the framework's Vite plugin and never overwritten once they exist. Edit the wrappers directly.
97
+
96
98
  See `experimental/custom/README.md` for the full prop contract.
97
99
 
98
100
  ---
@@ -171,6 +173,7 @@ ping-lf update --directory /path/to/project # target a specific project root
171
173
  **What it preserves**
172
174
 
173
175
  - `experimental/custom/` — all custom components are untouched
176
+ - The header and footer wrapper files are never overwritten — `update` only backfills ones that are missing, so your edits survive
174
177
  - `pnpm-workspace.yaml` — your workspace config is not overwritten
175
178
  - `.env` files — excluded from the copy
176
179
 
@@ -191,7 +194,9 @@ my-login-project/
191
194
  │ └── custom/
192
195
  │ ├── README.md # Component authoring guide
193
196
  │ ├── callbacks/ # Your custom callback components
194
- │ └── stages/ # Your custom stage components
197
+ │ ├── stages/ # Your custom stage components
198
+ │ ├── header/header.svelte # Page header wrapper (placed by init; regenerated by the plugin if deleted)
199
+ │ └── footer/footer.svelte # Page footer wrapper (placed by init; regenerated by the plugin if deleted)
195
200
  ├── .generator-version # Tracks the framework version in use
196
201
  └── pnpm-workspace.yaml
197
202
  ```
@@ -19,11 +19,21 @@ export declare const StageNameSchema: Schema.transform<Schema.filter<typeof Sche
19
19
  name: typeof Schema.String;
20
20
  slug: typeof Schema.String;
21
21
  }>>;
22
- export declare function scaffoldComponent(type: 'callback' | 'stage', name: string, directory?: string): Effect.Effect<void, import("../errors.js").GeneratorVersionError | InvalidComponentNameError | ComponentAlreadyExistsError | import("../errors.js").RegistryScanError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem | Path.Path>;
23
- export declare const generateCommand: Command.Command<"generate", FileSystem.FileSystem | Path.Path | import("@effect/platform/Terminal").Terminal, import("../errors.js").GeneratorVersionError | InvalidComponentNameError | ComponentAlreadyExistsError | import("../errors.js").RegistryScanError | import("@effect/platform/Error").PlatformError | import("@effect/platform/Terminal").QuitException, {
22
+ /**
23
+ * Resolves the templates directory relative to this file.
24
+ * In the compiled output (dist/src/commands/) the templates live one level up
25
+ * at dist/src/templates/. In Vitest (src/commands/) they live two levels up at
26
+ * the workspace root templates/ directory — the same fallback pattern used in mcp.ts.
27
+ */
28
+ export declare function getTemplatesDir(): string;
29
+ /** Supported custom component types for scaffolding. */
30
+ type ScaffoldType = 'callback' | 'stage';
31
+ export declare function scaffoldComponent(type: ScaffoldType, name: string, directory?: string): Effect.Effect<void, import("../errors.js").GeneratorVersionError | InvalidComponentNameError | ComponentAlreadyExistsError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem | Path.Path>;
32
+ export declare const generateCommand: Command.Command<"generate", FileSystem.FileSystem | Path.Path | import("@effect/platform/Terminal").Terminal, import("../errors.js").GeneratorVersionError | InvalidComponentNameError | ComponentAlreadyExistsError | import("@effect/platform/Error").PlatformError | import("@effect/platform/Terminal").QuitException, {
24
33
  readonly subcommand: import("effect/Option").Option<{
25
34
  readonly name: string;
26
35
  } | {
27
36
  readonly name: string;
28
37
  }>;
29
38
  }>;
39
+ export {};
@@ -7,7 +7,6 @@ import { fileURLToPath } from 'node:url';
7
7
  import { assertValidProject } from '../config/version.js';
8
8
  import { ComponentAlreadyExistsError, InvalidComponentNameError } from '../errors.js';
9
9
  import { expandTilde } from '../services/file-system.js';
10
- import { runRegistryScript } from '../services/registry.js';
11
10
  import { toPascalCase } from '../utils.js';
12
11
  const CUSTOM_DIR = 'experimental/custom';
13
12
  /**
@@ -66,7 +65,7 @@ export const StageNameSchema = Schema.String.pipe(Schema.filter((s) => s.length
66
65
  * at dist/src/templates/. In Vitest (src/commands/) they live two levels up at
67
66
  * the workspace root templates/ directory — the same fallback pattern used in mcp.ts.
68
67
  */
69
- function getTemplatesDir() {
68
+ export function getTemplatesDir() {
70
69
  const __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
71
70
  const compiledPath = nodePath.join(__dirname, '../templates');
72
71
  return existsSync(compiledPath) ? compiledPath : nodePath.join(__dirname, '../../templates');
@@ -77,14 +76,17 @@ export function scaffoldComponent(type, name, directory) {
77
76
  const p = yield* Path.Path;
78
77
  // ── Guard: validate component name format ─────────────────────────────
79
78
  // Callbacks must be valid name (PascalCase, alphanumeric only).
80
- // Stages are arbitrary AM strings with no naming convention constraint.
79
+ // Stages are arbitrary strings with no naming convention constraint.
81
80
  const nameSchema = type === 'callback' ? CallbackNameSchema : StageNameSchema;
82
81
  const { slug } = yield* Schema.decode(nameSchema)(name).pipe(Effect.mapError(() => new InvalidComponentNameError({ name })));
83
82
  const cwd = p.resolve(expandTilde(directory ?? process.cwd()));
84
83
  // ── Guard: must be run from an initialized project root ───────────────
85
84
  yield* assertValidProject(cwd);
86
- const subDir = type === 'callback' ? 'callbacks' : 'stages';
87
- const componentDir = p.join(cwd, CUSTOM_DIR, subDir, slug);
85
+ const subDirs = {
86
+ callback: 'callbacks',
87
+ stage: 'stages',
88
+ };
89
+ const componentDir = p.join(cwd, CUSTOM_DIR, subDirs[type], slug);
88
90
  const templatesDir = p.join(getTemplatesDir(), type);
89
91
  // ── Guard: prevent overwriting an existing component ──────────────────
90
92
  const exists = yield* fs.exists(componentDir);
@@ -111,10 +113,10 @@ export function scaffoldComponent(type, name, directory) {
111
113
  yield* fs.writeFileString(targetPath, processedContent);
112
114
  createdFiles.push(targetPath);
113
115
  }
114
- // ── Regenerate custom-registry.ts ────────────────────────────────────
115
- yield* Console.log('Regenerating custom component registry...');
116
- yield* runRegistryScript(cwd);
117
116
  // ── Print summary ──────────────────────────────────────────────────────
117
+ // custom-registry.ts is NOT regenerated here — the framework's Vite plugin
118
+ // picks the new component up on the next build or dev-server start (and,
119
+ // in dev, watches these directories for adds/removals without a restart).
118
120
  yield* Console.log(`Done. ${type} component scaffolded successfully.\n\n` +
119
121
  `Files created:\n` +
120
122
  createdFiles.map((f) => ` ${f}`).join('\n') +
@@ -3,13 +3,18 @@ import { FileSystem, Path } from '@effect/platform';
3
3
  import { Effect } from 'effect';
4
4
  import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
5
5
  import type { Option } from 'effect';
6
+ /**
7
+ * Custom component directories scaffolded (with .gitkeep) at project init.
8
+ * Protected during `ping-lf update` — see PROTECTED_DIRS in services/file-system.
9
+ */
10
+ export declare const CUSTOM_COMPONENT_DIRS: readonly ["callbacks", "stages"];
6
11
  export interface InitProjectOptions {
7
12
  readonly directory: string;
8
13
  readonly local: Option.Option<string>;
9
14
  readonly version: Option.Option<string>;
10
15
  }
11
- export declare const initProject: ({ directory, local, version }: InitProjectOptions) => Effect.Effect<void, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | DirectoryConflictError | DirectoryNotEmptyError | import("../errors.js").RegistryScanError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem | Path.Path | import("../services/release.js").Release>;
12
- export declare const initCommand: Command.Command<"init", FileSystem.FileSystem | Path.Path | import("../services/release.js").Release, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | DirectoryConflictError | DirectoryNotEmptyError | import("../errors.js").RegistryScanError | import("@effect/platform/Error").PlatformError, {
16
+ export declare const initProject: ({ directory, local, version }: InitProjectOptions) => Effect.Effect<void, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | DirectoryConflictError | DirectoryNotEmptyError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem | Path.Path | import("../services/release.js").Release>;
17
+ export declare const initCommand: Command.Command<"init", FileSystem.FileSystem | Path.Path | import("../services/release.js").Release, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | DirectoryConflictError | DirectoryNotEmptyError | import("@effect/platform/Error").PlatformError, {
13
18
  readonly directory: string;
14
19
  readonly local: Option.Option<string>;
15
20
  readonly tag: Option.Option<string>;
@@ -3,8 +3,8 @@ import { FileSystem, Path } from '@effect/platform';
3
3
  import { Console, Effect } from 'effect';
4
4
  import { writeVersion } from '../config/version.js';
5
5
  import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
6
- import { copyWithExclusions, expandTilde, isFrameworkDirectory } from '../services/file-system.js';
7
- import { runRegistryScript } from '../services/registry.js';
6
+ import { copyWithExclusions, copyWrapperFiles, expandTilde, isFrameworkDirectory, } from '../services/file-system.js';
7
+ import { getTemplatesDir } from './generate.js';
8
8
  import { resolveSource } from './source.js';
9
9
  /**
10
10
  * Minimal pnpm-workspace.yaml written to the customer project.
@@ -14,6 +14,11 @@ const PNPM_WORKSPACE = `packages:
14
14
  - 'apps/*'
15
15
  - 'e2e'
16
16
  `;
17
+ /**
18
+ * Custom component directories scaffolded (with .gitkeep) at project init.
19
+ * Protected during `ping-lf update` — see PROTECTED_DIRS in services/file-system.
20
+ */
21
+ export const CUSTOM_COMPONENT_DIRS = ['callbacks', 'stages'];
17
22
  const nextStepsMessage = (dir) => `
18
23
  Done. Project initialized successfully.
19
24
 
@@ -57,6 +62,12 @@ export const initProject = ({ directory, local, version }) => Effect.gen(functio
57
62
  yield* Console.log('Copying framework files...');
58
63
  yield* fs.makeDirectory(resolvedDir, { recursive: true });
59
64
  yield* copyWithExclusions(sourceDir, resolvedDir);
65
+ // ── 2b. Place the framework-shipped wrapper files ──────────────────
66
+ // The bulk copy skips them (they are file-protected: customer edits
67
+ // to them must survive `update`), so init places them explicitly from
68
+ // the CLI's own wrapper templates. The app's +page.svelte imports them
69
+ // directly, so a project without them cannot build.
70
+ yield* copyWrapperFiles(resolvedDir, getTemplatesDir());
60
71
  return resolvedVersion;
61
72
  }));
62
73
  // ── 3. Write pnpm-workspace.yaml (omits tools/ present in upstream) ────
@@ -64,26 +75,20 @@ export const initProject = ({ directory, local, version }) => Effect.gen(functio
64
75
  // ── 4. Scaffold experimental/custom/ ──────────────────────────────────
65
76
  // copyWithExclusions already copies experimental/custom/demo/,
66
77
  // login-framework.ts, README.md, and tsconfig.json from the source.
67
- // Only callbacks/ and stages/ are protected (they hold user components).
78
+ // Only the custom component dirs are protected (they hold user components).
68
79
  // Create them as empty dirs with .gitkeep — components come via `ping-lf generate`.
69
80
  yield* Console.log('Scaffolding custom component directories...');
70
- yield* Effect.forEach(['callbacks', 'stages'], (dir) => Effect.gen(function* () {
81
+ yield* Effect.forEach(CUSTOM_COMPONENT_DIRS, (dir) => Effect.gen(function* () {
71
82
  const customDir = path.join(resolvedDir, 'experimental', 'custom', dir);
72
83
  yield* fs.makeDirectory(customDir, { recursive: true });
73
84
  yield* fs.writeFileString(path.join(customDir, '.gitkeep'), '');
74
85
  }), { concurrency: 'unbounded', discard: true });
75
- // ── 5. Generate custom-registry.ts ────────────────────────────────────
76
- // Run the canonical registry script so the file is immediately present
77
- // with the correct CustomRegistryEntry format. The empty callbacks/ and
78
- // stages/ dirs produce empty registries — no custom components yet.
79
- yield* Console.log('Generating custom component registry...');
80
- yield* runRegistryScript(resolvedDir);
81
- // ── 6. Write .generator-version ────────────────────────────────────────
86
+ // ── 5. Write .generator-version ────────────────────────────────────────
82
87
  yield* writeVersion(resolvedDir, {
83
88
  version: resolvedVersion,
84
89
  generatedAt: new Date().toISOString(),
85
90
  });
86
- // ── 7. Print next steps ───────────────────────────────────────────────
91
+ // ── 6. Print next steps ───────────────────────────────────────────────
87
92
  yield* Console.log(nextStepsMessage(directory));
88
93
  });
89
94
  export const initCommand = Command.make('init', {
@@ -1,6 +1,12 @@
1
1
  import { Command } from '@effect/cli';
2
- import { Option } from 'effect';
3
- export declare const updateCommand: Command.Command<"update", import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path | import("../services/release.js").Release, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | import("../errors.js").GeneratorVersionError | import("../errors.js").RegistryScanError | import("@effect/platform/Error").PlatformError, {
2
+ import { Effect, Option } from 'effect';
3
+ export interface UpdateProjectOptions {
4
+ readonly directory: Option.Option<string>;
5
+ readonly local: Option.Option<string>;
6
+ readonly tag: Option.Option<string>;
7
+ }
8
+ export declare const updateProject: ({ directory, local, tag }: UpdateProjectOptions) => Effect.Effect<void, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | import("../errors.js").GeneratorVersionError | import("@effect/platform/Error").PlatformError, import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path | import("../services/release.js").Release>;
9
+ export declare const updateCommand: Command.Command<"update", import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path | import("../services/release.js").Release, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | import("../errors.js").GeneratorVersionError | import("@effect/platform/Error").PlatformError, {
4
10
  readonly directory: Option.Option<string>;
5
11
  readonly local: Option.Option<string>;
6
12
  readonly tag: Option.Option<string>;
@@ -2,14 +2,10 @@ import { Command, Options } from '@effect/cli';
2
2
  import { Console, Effect, Option } from 'effect';
3
3
  import nodePath from 'node:path';
4
4
  import { assertValidProject, writeVersion } from '../config/version.js';
5
- import { copyWithExclusions, expandTilde } from '../services/file-system.js';
6
- import { runRegistryScript } from '../services/registry.js';
5
+ import { copyWithExclusions, copyWrapperFiles, expandTilde } from '../services/file-system.js';
6
+ import { getTemplatesDir } from './generate.js';
7
7
  import { resolveSource } from './source.js';
8
- export const updateCommand = Command.make('update', {
9
- directory: Options.optional(Options.text('directory').pipe(Options.withDescription('Project root directory. Defaults to the current working directory.'))),
10
- local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
11
- tag: Options.optional(Options.text('tag').pipe(Options.withDescription('Framework Git release tag to update to (e.g. @forgerock/login-widget@2.1.0 or v1.2.0). If omitted, the main branch is used.'))),
12
- }, ({ local, tag, directory }) => Effect.gen(function* () {
8
+ export const updateProject = ({ directory, local, tag }) => Effect.gen(function* () {
13
9
  const cwd = nodePath.resolve(expandTilde(Option.getOrElse(directory, () => process.cwd())));
14
10
  // ── 1. Verify this is an initialized project ──────────────────────────
15
11
  const currentVersion = yield* assertValidProject(cwd);
@@ -21,17 +17,28 @@ export const updateCommand = Command.make('update', {
21
17
  const { sourceDir, resolvedVersion } = yield* resolveSource(local, tag, cwd);
22
18
  yield* Console.log('Copying updated framework files...');
23
19
  yield* copyWithExclusions(sourceDir, cwd);
20
+ // ── 2b. Backfill missing wrapper files ─────────────────────────────
21
+ // The bulk copy skips them (they are file-protected so customer edits
22
+ // survive), but a project updated from an older framework version may
23
+ // not have them yet — the app's +page.svelte imports them directly, so
24
+ // a project missing one cannot build. copyWrapperFiles copies only
25
+ // absent files, from the CLI's own wrapper templates, never
26
+ // overwriting customer edits.
27
+ yield* copyWrapperFiles(cwd, getTemplatesDir());
24
28
  return resolvedVersion;
25
29
  }));
26
- // ── 3. Regenerate custom-registry.ts ──────────────────────────────────
27
- yield* Console.log('Regenerating custom component registry...');
28
- yield* runRegistryScript(cwd);
29
- // ── 4. Update .generator-version ──────────────────────────────────────
30
+ // ── 3. Update .generator-version ──────────────────────────────────────
31
+ // custom-registry.ts is NOT regenerated here — the framework's Vite
32
+ // plugin regenerates it on the next build or dev-server start.
30
33
  yield* writeVersion(cwd, {
31
34
  version: resolvedVersion,
32
35
  generatedAt: new Date().toISOString(),
33
36
  });
34
37
  yield* Console.log(`\nDone. Updated from ${currentVersion.version} to ${resolvedVersion}.\n` +
35
- 'Custom component registry regenerated.\n' +
36
38
  'Run "pnpm install" if package dependencies changed.\n');
37
- })).pipe(Command.withDescription('Fetch the latest (or a specified) framework version and overwrite core files while preserving experimental/custom/.'));
39
+ });
40
+ export const updateCommand = Command.make('update', {
41
+ directory: Options.optional(Options.text('directory').pipe(Options.withDescription('Project root directory. Defaults to the current working directory.'))),
42
+ local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
43
+ tag: Options.optional(Options.text('tag').pipe(Options.withDescription('Framework Git release tag to update to (e.g. @forgerock/login-widget@2.1.0 or v1.2.0). If omitted, the main branch is used.'))),
44
+ }, updateProject).pipe(Command.withDescription('Fetch the latest (or a specified) framework version and overwrite core files while preserving experimental/custom/.'));
@@ -79,12 +79,4 @@ export declare class ComponentAlreadyExistsError extends ComponentAlreadyExistsE
79
79
  readonly path: string;
80
80
  }> {
81
81
  }
82
- 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 & {
83
- readonly _tag: "RegistryScanError";
84
- } & Readonly<A>;
85
- export declare class RegistryScanError extends RegistryScanError_base<{
86
- readonly directory: string;
87
- readonly cause?: unknown;
88
- }> {
89
- }
90
82
  export {};
@@ -21,5 +21,3 @@ export class InvalidComponentNameError extends Data.TaggedError('InvalidComponen
21
21
  }
22
22
  export class ComponentAlreadyExistsError extends Data.TaggedError('ComponentAlreadyExistsError') {
23
23
  }
24
- export class RegistryScanError extends Data.TaggedError('RegistryScanError') {
25
- }
package/dist/src/mcp.js CHANGED
@@ -10,7 +10,6 @@ import { initProject } from './commands/init.js';
10
10
  import { resolveSource } from './commands/source.js';
11
11
  import { assertValidProject, writeVersion } from './config/version.js';
12
12
  import { copyWithExclusions, expandTilde } from './services/file-system.js';
13
- import { runRegistryScript } from './services/registry.js';
14
13
  import { GithubReleaseLayer, Release } from './services/release.js';
15
14
  // Resolve package.json from either dist/src/ (compiled) or src/ (Vitest).
16
15
  const __dir = dirname(fileURLToPath(import.meta.url));
@@ -38,8 +37,6 @@ function formatError(cause) {
38
37
  return `Filesystem error (${e['operation']}) at "${e['path']}": ${e['cause']}`;
39
38
  case 'GeneratorVersionError':
40
39
  return `Generator version error: ${e['message']}${e['path'] ? ` (${e['path']})` : ''}`;
41
- case 'RegistryScanError':
42
- return `Registry scan failed in "${e['directory']}": ${e['cause']}`;
43
40
  case 'DirectoryConflictError':
44
41
  return `"${e['path']}" already contains a framework project. Use local path instead.`;
45
42
  case 'DirectoryNotEmptyError':
@@ -158,7 +155,6 @@ const handlerLayer = mcpToolkit.toLayer({
158
155
  yield* copyWithExclusions(sourceDir, cwd);
159
156
  return resolvedVersion;
160
157
  }));
161
- yield* runRegistryScript(cwd);
162
158
  yield* writeVersion(cwd, {
163
159
  version: resolvedVersion,
164
160
  generatedAt: new Date().toISOString(),
@@ -1,4 +1,4 @@
1
- import { FileSystem } from '@effect/platform';
1
+ import { FileSystem, Path } from '@effect/platform';
2
2
  import { Effect } from 'effect';
3
3
  import { FileSystemError } from '../errors.js';
4
4
  /**
@@ -8,6 +8,37 @@ import { FileSystemError } from '../errors.js';
8
8
  * that the directory was created by `ping-lf init`.
9
9
  */
10
10
  export declare const isFrameworkDirectory: (dir: string) => Effect.Effect<boolean, never, FileSystem.FileSystem>;
11
+ /**
12
+ * Framework-shipped custom wrapper files. Each is a fixed, user-editable file
13
+ * rendered by the login app (`+page.svelte` imports it directly). The bulk
14
+ * copy skips them so `update` can never overwrite customer edits;
15
+ * `copyWrapperFiles` backfills any the target is missing from the CLI's own
16
+ * wrapper templates (see WRAPPER_TEMPLATE_ELEMENTS) so a project updated onto
17
+ * a framework version whose app imports the wrappers still gets the defaults.
18
+ */
19
+ export declare const CUSTOM_WRAPPER_FILES: readonly ["experimental/custom/header/header.svelte", "experimental/custom/footer/footer.svelte"];
20
+ /**
21
+ * The page elements that ship a wrapper file. `copyWrapperFiles` copies
22
+ * `templates/wrapper/<element>.svelte` (the CLI's own shipped templates,
23
+ * resolved like the callback/stage scaffold templates) into
24
+ * `experimental/custom/<element>/<element>.svelte`.
25
+ *
26
+ * The CLI never reads the framework's `core/` tree — build-time vs. runtime
27
+ * boundary — so it owns copies of the wrapper templates instead of importing
28
+ * the framework's `core/.../wrapper-templates/`. The per-element order
29
+ * matches CUSTOM_WRAPPER_FILES.
30
+ */
31
+ export declare const WRAPPER_TEMPLATE_ELEMENTS: readonly ["header", "footer"];
11
32
  /** Expand a leading `~` to the user's home directory. */
12
33
  export declare function expandTilde(p: string): string;
13
34
  export declare const copyWithExclusions: (sourceDir: string, targetDir: string) => Effect.Effect<void, FileSystemError, FileSystem.FileSystem>;
35
+ /**
36
+ * Backfills the framework-shipped wrapper files into `targetDir`, skipping any
37
+ * the target already has. Customer edits are never overwritten — a file is
38
+ * copied only when absent. The copy reads from the CLI's own wrapper templates
39
+ * (`templates/wrapper/`, passed as `templatesDir`), mirroring the
40
+ * callback/stage scaffold templates. A missing template is a no-op, which
41
+ * keeps older CLI versions (whose paired framework app does not import
42
+ * wrappers) safe.
43
+ */
44
+ export declare const copyWrapperFiles: (targetDir: string, templatesDir: string) => Effect.Effect<void, FileSystemError, FileSystem.FileSystem | Path.Path>;
@@ -1,4 +1,4 @@
1
- import { FileSystem } from '@effect/platform';
1
+ import { FileSystem, Path } from '@effect/platform';
2
2
  import { Effect } from 'effect';
3
3
  import path from 'node:path';
4
4
  import { isExcluded } from '../config/exclusions.js';
@@ -23,6 +23,30 @@ export const isFrameworkDirectory = Effect.fnUntraced(function* (dir) {
23
23
  return hasPkgJson && (hasExperimental || hasPnpmWorkspace);
24
24
  });
25
25
  const PROTECTED_DIRS = ['experimental/custom/callbacks/', 'experimental/custom/stages/'];
26
+ /**
27
+ * Framework-shipped custom wrapper files. Each is a fixed, user-editable file
28
+ * rendered by the login app (`+page.svelte` imports it directly). The bulk
29
+ * copy skips them so `update` can never overwrite customer edits;
30
+ * `copyWrapperFiles` backfills any the target is missing from the CLI's own
31
+ * wrapper templates (see WRAPPER_TEMPLATE_ELEMENTS) so a project updated onto
32
+ * a framework version whose app imports the wrappers still gets the defaults.
33
+ */
34
+ export const CUSTOM_WRAPPER_FILES = [
35
+ 'experimental/custom/header/header.svelte',
36
+ 'experimental/custom/footer/footer.svelte',
37
+ ];
38
+ /**
39
+ * The page elements that ship a wrapper file. `copyWrapperFiles` copies
40
+ * `templates/wrapper/<element>.svelte` (the CLI's own shipped templates,
41
+ * resolved like the callback/stage scaffold templates) into
42
+ * `experimental/custom/<element>/<element>.svelte`.
43
+ *
44
+ * The CLI never reads the framework's `core/` tree — build-time vs. runtime
45
+ * boundary — so it owns copies of the wrapper templates instead of importing
46
+ * the framework's `core/.../wrapper-templates/`. The per-element order
47
+ * matches CUSTOM_WRAPPER_FILES.
48
+ */
49
+ export const WRAPPER_TEMPLATE_ELEMENTS = ['header', 'footer'];
26
50
  /** Expand a leading `~` to the user's home directory. */
27
51
  export function expandTilde(p) {
28
52
  if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
@@ -33,7 +57,8 @@ export function expandTilde(p) {
33
57
  }
34
58
  function isProtected(relativePath) {
35
59
  const normalized = normalizeSeparators(relativePath);
36
- return PROTECTED_DIRS.some((dir) => `${normalized}/`.startsWith(dir));
60
+ return (PROTECTED_DIRS.some((dir) => `${normalized}/`.startsWith(dir)) ||
61
+ CUSTOM_WRAPPER_FILES.includes(normalized));
37
62
  }
38
63
  const walk = (fs, sourceDir, targetDir, dir) => Effect.gen(function* () {
39
64
  const entries = yield* fs.readDirectory(dir).pipe(wrapFsCall('readDirectory', dir));
@@ -62,3 +87,40 @@ export const copyWithExclusions = (sourceDir, targetDir) => Effect.gen(function*
62
87
  const fs = yield* FileSystem.FileSystem;
63
88
  yield* walk(fs, sourceDir, targetDir, sourceDir);
64
89
  });
90
+ /**
91
+ * Backfills the framework-shipped wrapper files into `targetDir`, skipping any
92
+ * the target already has. Customer edits are never overwritten — a file is
93
+ * copied only when absent. The copy reads from the CLI's own wrapper templates
94
+ * (`templates/wrapper/`, passed as `templatesDir`), mirroring the
95
+ * callback/stage scaffold templates. A missing template is a no-op, which
96
+ * keeps older CLI versions (whose paired framework app does not import
97
+ * wrappers) safe.
98
+ */
99
+ export const copyWrapperFiles = (targetDir, templatesDir) => Effect.gen(function* () {
100
+ const fs = yield* FileSystem.FileSystem;
101
+ const path = yield* Path.Path;
102
+ yield* Effect.forEach(
103
+ // Pair each wrapper target element with its template source. Positional
104
+ // zip on two pinned constants: the contract test pins both arrays and
105
+ // their per-element order, so an accidental desync fails the build.
106
+ WRAPPER_TEMPLATE_ELEMENTS.map((element, index) => ({
107
+ element,
108
+ wrapperPath: CUSTOM_WRAPPER_FILES[index],
109
+ })), ({ element, wrapperPath }) => Effect.gen(function* () {
110
+ const sourcePath = path.join(templatesDir, 'wrapper', `${element}.svelte`);
111
+ const targetPath = path.join(targetDir, wrapperPath);
112
+ const hasSource = yield* fs.exists(sourcePath).pipe(Effect.orElseSucceed(() => false));
113
+ if (!hasSource) {
114
+ return;
115
+ }
116
+ const hasTarget = yield* fs.exists(targetPath).pipe(Effect.orElseSucceed(() => false));
117
+ if (hasTarget) {
118
+ return;
119
+ }
120
+ const parentDir = path.dirname(targetPath);
121
+ yield* fs
122
+ .makeDirectory(parentDir, { recursive: true })
123
+ .pipe(wrapFsCall('makeDirectory', parentDir));
124
+ yield* fs.copyFile(sourcePath, targetPath).pipe(wrapFsCall('copyFile', sourcePath));
125
+ }), { concurrency: 'unbounded', discard: true });
126
+ });
@@ -0,0 +1,68 @@
1
+ <!--
2
+ Custom page footer wrapper.
3
+ ─────────────────────────────
4
+ This is your file: the framework renders it below the journey container
5
+ (inside the root div of the login app's +page.svelte). It is never
6
+ overwritten once it exists — not by rebuilds, not by `ping-lf update` — so
7
+ edit it freely; your changes survive framework upgrades. If you ever delete
8
+ it, the next build or dev-server start regenerates the default (only when
9
+ the file is absent; your edits are never touched).
10
+
11
+ HOW IT RENDERS
12
+ ──────────────
13
+ The footer mounts client-side only: it first renders once a journey step
14
+ arrives, then stays mounted for the rest of the session (no flicker while
15
+ steps load, switch journeys, or redirect on completion).
16
+
17
+ PROPS
18
+ ─────
19
+ The wrapper receives exactly one prop:
20
+
21
+ journeyName: string
22
+ Name of the journey currently driving the page (e.g. "Login",
23
+ "Register"). `''` means the AM default journey — the one the tenant
24
+ serves when the URL names no journey. Show your default-journey view
25
+ in that case; the footer always renders something.
26
+
27
+ The framework passes no other props. Do not add required props of your own —
28
+ they will arrive undefined. Anything you need beyond `journeyName`, derive
29
+ inside this component from the framework exports.
30
+ -->
31
+
32
+ <script lang="ts">
33
+ // ─── Framework imports ──────────────────────────────────────────────────────
34
+ /**
35
+ * Import everything you need from '$login-framework' — the
36
+ * framework's centralized exports for custom components. No need to reach
37
+ * into internal aliases like $core, $components, or $journey directly.
38
+ *
39
+ * Available exports (see experimental/custom/login-framework.ts for the full list).
40
+ */
41
+ import type { CustomWrapperProps } from '$login-framework';
42
+
43
+ /** The one prop the framework passes: the active journey name (`''` = default). */
44
+ export let journeyName: CustomWrapperProps['journeyName'] = '';
45
+ </script>
46
+
47
+ <!--
48
+ Default markup: neutral no-op placeholder. Replace it with your own branding.
49
+
50
+ Example — branch the markup on the prop value. Render the copyright line
51
+ plus "Create your account" when journeyName is "Register", the plain
52
+ copyright line when it is the empty string (that is the AM default journey,
53
+ the case where the URL named no journey), and a journeyName-suffixed
54
+ copyright line otherwise. In Svelte that is an if/else if/else chain on
55
+ journeyName.
56
+
57
+ All Tailwind classes use the framework's tw_ prefix so theme compilation
58
+ picks them up (see themes/default/config.cjs — prefix: 'tw_').
59
+ -->
60
+ <footer class="tw_hidden"></footer>
61
+
62
+ <!--
63
+ Scoped styles for this wrapper. They apply only here — they won't leak to the
64
+ rest of the widget.
65
+ -->
66
+ <style>
67
+ /* Add your custom footer styles here. */
68
+ </style>
@@ -0,0 +1,67 @@
1
+ <!--
2
+ Custom page header wrapper.
3
+ ─────────────────────────────
4
+ This is your file: the framework renders it above the journey container
5
+ (inside the root div of the login app's +page.svelte). It is never
6
+ overwritten once it exists — not by rebuilds, not by `ping-lf update` — so
7
+ edit it freely; your changes survive framework upgrades. If you ever delete
8
+ it, the next build or dev-server start regenerates the default (only when
9
+ the file is absent; your edits are never touched).
10
+
11
+ HOW IT RENDERS
12
+ ──────────────
13
+ The header mounts client-side only: it first renders once a journey step
14
+ arrives, then stays mounted for the rest of the session (no flicker while
15
+ steps load, switch journeys, or redirect on completion).
16
+
17
+ PROPS
18
+ ─────
19
+ The wrapper receives exactly one prop:
20
+
21
+ journeyName: string
22
+ Name of the journey currently driving the page (e.g. "Login",
23
+ "Register"). `''` means the AM default journey — the one the tenant
24
+ serves when the URL names no journey. Show your default-journey view
25
+ in that case; the header always renders something.
26
+
27
+ The framework passes no other props. Do not add required props of your own —
28
+ they will arrive undefined. Anything you need beyond `journeyName`, derive
29
+ inside this component from the framework exports.
30
+ -->
31
+
32
+ <script lang="ts">
33
+ // ─── Framework imports ──────────────────────────────────────────────────────
34
+ /**
35
+ * Import everything you need from '$login-framework' — the
36
+ * framework's centralized exports for custom components. No need to reach
37
+ * into internal aliases like $core, $components, or $journey directly.
38
+ *
39
+ * Available exports (see experimental/custom/login-framework.ts for the full list).
40
+ */
41
+ import type { CustomWrapperProps } from '$login-framework';
42
+
43
+ /** The one prop the framework passes: the active journey name (`''` = default). */
44
+ export let journeyName: CustomWrapperProps['journeyName'] = '';
45
+ </script>
46
+
47
+ <!--
48
+ Default markup: neutral no-op placeholder. Replace it with your own branding.
49
+
50
+ Example — branch the markup on the prop value. Render a "Register" banner
51
+ when journeyName is "Register", a default-journey banner when it is the
52
+ empty string (that is the AM default journey, the case where the URL named
53
+ no journey), and a journeyName-driven banner otherwise. In Svelte that is
54
+ an if/else if/else chain on journeyName.
55
+
56
+ All Tailwind classes use the framework's tw_ prefix so theme compilation
57
+ picks them up (see themes/default/config.cjs — prefix: 'tw_').
58
+ -->
59
+ <header class="tw_hidden"></header>
60
+
61
+ <!--
62
+ Scoped styles for this wrapper. They apply only here — they won't leak to the
63
+ rest of the widget.
64
+ -->
65
+ <style>
66
+ /* Add your custom header styles here. */
67
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgerock/login-framework-cli",
3
- "version": "0.0.0-beta-20261013223421",
3
+ "version": "0.0.0-beta-20261021173425",
4
4
  "description": "CLI tool for scaffolding and managing Ping Login Widget and Login App custom component projects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,10 +44,10 @@
44
44
  "@effect/printer-ansi": "^0.49.0",
45
45
  "@effect/typeclass": "^0.40.0",
46
46
  "effect": "^3.21.0",
47
- "svelte": "^5.55.7",
48
47
  "tar": "^7.5.21"
49
48
  },
50
49
  "devDependencies": {
50
+ "@effect/vitest": "0.29.0",
51
51
  "typescript": "^5.2.2",
52
52
  "vitest": "^3.2.6"
53
53
  },
@@ -1,29 +0,0 @@
1
- import { FileSystem, Path } from '@effect/platform';
2
- import { Effect } from 'effect';
3
- import { RegistryScanError } from '../errors.js';
4
- type ComponentType = 'stage' | 'callback';
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. */
12
- export declare function parseAcceptedProps(content: string): string[];
13
- /** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
14
- export declare const parseComponentHeader: (filePath: string, content: string) => Effect.Effect<{
15
- type: ComponentType;
16
- name: string;
17
- }, RegistryScanError>;
18
- export declare function buildRegistryContent(path: Path.Path, registryDir: string, stageComponents: ComponentEntry[], callbackComponents: ComponentEntry[]): string;
19
- /**
20
- * Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
21
- * `@component`-annotated Svelte files and writes
22
- * `core/journey/_utilities/custom-registry.ts`.
23
- *
24
- * All I/O runs in-process via the platform `FileSystem` service — no subprocess
25
- * spawning. Validation errors across multiple components are collected and
26
- * reported together.
27
- */
28
- export declare const runRegistryScript: (projectDir: string) => Effect.Effect<void, RegistryScanError, FileSystem.FileSystem | Path.Path>;
29
- export {};
@@ -1,176 +0,0 @@
1
- import { FileSystem, Path } from '@effect/platform';
2
- import { Console, Effect } from 'effect';
3
- import { parse } from 'svelte/compiler';
4
- import { RegistryScanError } from '../errors.js';
5
- // --------------------------------------------------------------------------
6
- // Helpers (exported for testing)
7
- // --------------------------------------------------------------------------
8
- /** Extracts names of all `export let` prop declarations from a Svelte component's instance script. */
9
- export function parseAcceptedProps(content) {
10
- const abstractSyntaxTree = parse(content, { modern: false });
11
- const props = [];
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
- }
24
- return props;
25
- }
26
- function toPascalCase(str) {
27
- return str
28
- .replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
29
- .replace(/^(.)/, (_, chr) => chr.toUpperCase());
30
- }
31
- /** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
32
- export const parseComponentHeader = (filePath, content) => {
33
- const fail = (cause) => Effect.fail(new RegistryScanError({ directory: filePath, cause }));
34
- const commentMatch = content.match(/^<!--([\s\S]*?)-->/);
35
- if (!commentMatch)
36
- return fail('Missing @component header. Every custom component must begin with:\n' +
37
- '<!--\n @component\n Type: stage|callback\n Name: <ComponentName>\n -->');
38
- const block = commentMatch[1];
39
- if (!block.includes('@component'))
40
- return fail('Missing "@component" tag in the opening comment.');
41
- const typeMatch = block.match(/Type:\s*(\S+)/);
42
- if (!typeMatch)
43
- return fail('Missing "Type:" field in @component header. Expected: Type: stage or Type: callback');
44
- const rawType = typeMatch[1].toLowerCase();
45
- if (rawType !== 'stage' && rawType !== 'callback')
46
- return fail(`Invalid Type value "${typeMatch[1]}". Must be "stage" or "callback".`);
47
- const nameMatch = block.match(/Name:\s*(.+)/);
48
- if (!nameMatch)
49
- return fail('Missing "Name:" field in @component header. Expected: Name: <ComponentName>');
50
- return Effect.succeed({ type: rawType, name: nameMatch[1].trim() });
51
- };
52
- // --------------------------------------------------------------------------
53
- // File scanning
54
- // --------------------------------------------------------------------------
55
- const findSvelteFiles = (fs, path, dir) => fs.exists(dir).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((exists) => !exists
56
- ? Effect.succeed([])
57
- : fs.readDirectory(dir).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: dir, cause })), Effect.flatMap((entries) => Effect.forEach(entries, (entry) => {
58
- const fullPath = path.join(dir, entry);
59
- return fs.stat(fullPath).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: dir, cause })), Effect.flatMap((stat) => stat.type === 'Directory'
60
- ? findSvelteFiles(fs, path, fullPath)
61
- : Effect.succeed(entry.endsWith('.svelte') && !entry.endsWith('.story.svelte')
62
- ? [fullPath]
63
- : [])));
64
- }, { concurrency: 'unbounded' })), Effect.map((nested) => nested.flat()))));
65
- 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
66
- ? Effect.fail(new RegistryScanError({
67
- directory: filePath,
68
- cause: `Component in /experimental/custom/${expectedType}s/ declares Type: "${type}". Must declare Type: ${expectedType}`,
69
- }))
70
- : Effect.succeed({
71
- filePath,
72
- name,
73
- type,
74
- acceptedProps: parseAcceptedProps(content),
75
- })))))).pipe(Effect.mapError((errors) => new RegistryScanError({
76
- directory: dir,
77
- cause: errors.map((e) => String(e.cause)).join('\n'),
78
- })))));
79
- // --------------------------------------------------------------------------
80
- // Registry content builder (pure)
81
- // --------------------------------------------------------------------------
82
- export function buildRegistryContent(path, registryDir, stageComponents, callbackComponents) {
83
- const toEntry = (prefix) => ({ filePath, name, acceptedProps }) => {
84
- const relPath = path.relative(registryDir, filePath).replace(/\\/g, '/');
85
- const importPath = relPath.startsWith('.') ? relPath : `./${relPath}`;
86
- return { varName: `${prefix}${toPascalCase(name)}`, importPath, name, acceptedProps };
87
- };
88
- const stageEntries = stageComponents.map(toEntry('Stage'));
89
- const callbackEntries = callbackComponents.map(toEntry('Callback'));
90
- const lines = [
91
- `/**`,
92
- ` * AUTO-GENERATED — do not edit by hand.`,
93
- ` * Run \`pnpm build:widget\` or \`pnpm --filter @forgerock/login-widget exec vite build\` to regenerate.`,
94
- ` *`,
95
- ` * Source: /experimental/custom/stages/ and /experimental/custom/callbacks/`,
96
- ` */`,
97
- ``,
98
- `import type { Component } from 'svelte';`,
99
- ``,
100
- `export interface CustomRegistryEntry {`,
101
- ` component: Component;`,
102
- ` /** Props declared via \`export let\` in the component — only these are forwarded by the mapper. */`,
103
- ` acceptedProps: string[];`,
104
- `}`,
105
- ``,
106
- ];
107
- if (stageEntries.length > 0) {
108
- lines.push(`// Stage overrides / extensions`);
109
- for (const { varName, importPath } of stageEntries)
110
- lines.push(`import ${varName} from '${importPath}';`);
111
- lines.push(``);
112
- }
113
- if (callbackEntries.length > 0) {
114
- lines.push(`// Callback overrides / extensions`);
115
- for (const { varName, importPath } of callbackEntries)
116
- lines.push(`import ${varName} from '${importPath}';`);
117
- lines.push(``);
118
- }
119
- lines.push(`export const customStageRegistry: Record<string, CustomRegistryEntry> = {`);
120
- for (const { varName, name, acceptedProps } of stageEntries)
121
- lines.push(` ${JSON.stringify(name)}: { get component() { return ${varName}; }, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
122
- lines.push(`};`);
123
- lines.push(``);
124
- lines.push(`export const customCallbackRegistry: Record<string, CustomRegistryEntry> = {`);
125
- for (const { varName, name, acceptedProps } of callbackEntries)
126
- lines.push(` ${JSON.stringify(name)}: { get component() { return ${varName}; }, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
127
- lines.push(`};`);
128
- lines.push(``);
129
- return lines.join('\n');
130
- }
131
- // --------------------------------------------------------------------------
132
- // Public API
133
- // --------------------------------------------------------------------------
134
- /**
135
- * Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
136
- * `@component`-annotated Svelte files and writes
137
- * `core/journey/_utilities/custom-registry.ts`.
138
- *
139
- * All I/O runs in-process via the platform `FileSystem` service — no subprocess
140
- * spawning. Validation errors across multiple components are collected and
141
- * reported together.
142
- */
143
- export const runRegistryScript = (projectDir) => Effect.gen(function* () {
144
- const fs = yield* FileSystem.FileSystem;
145
- const path = yield* Path.Path;
146
- const stageDir = path.join(projectDir, 'experimental', 'custom', 'stages');
147
- const callbackDir = path.join(projectDir, 'experimental', 'custom', 'callbacks');
148
- const registryDir = path.join(projectDir, 'core', 'journey', '_utilities', 'registry');
149
- const registryPath = path.join(registryDir, 'custom-registry.ts');
150
- const [stageComponents, callbackComponents] = yield* Effect.all([
151
- scanDirectory(fs, path, stageDir, 'stage'),
152
- scanDirectory(fs, path, callbackDir, 'callback'),
153
- ], { concurrency: 'unbounded' });
154
- const content = buildRegistryContent(path, registryDir, stageComponents, callbackComponents);
155
- yield* fs
156
- .makeDirectory(registryDir, { recursive: true })
157
- .pipe(Effect.mapError((cause) => new RegistryScanError({ directory: registryDir, cause })));
158
- yield* fs
159
- .writeFileString(registryPath, content)
160
- .pipe(Effect.mapError((cause) => new RegistryScanError({ directory: registryPath, cause })));
161
- const total = stageComponents.length + callbackComponents.length;
162
- if (total === 0) {
163
- yield* Console.log(`custom-registry.ts generated (no custom components found — registries are empty)`);
164
- }
165
- else {
166
- yield* Console.log(`custom-registry.ts generated:`);
167
- if (stageComponents.length > 0)
168
- yield* Console.log(` Stages (${stageComponents.length}): ${stageComponents
169
- .map((c) => c.name)
170
- .join(', ')}`);
171
- if (callbackComponents.length > 0)
172
- yield* Console.log(` Callbacks (${callbackComponents.length}): ${callbackComponents
173
- .map((c) => c.name)
174
- .join(', ')}`);
175
- }
176
- });