@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2025 Ping Identity Corporation (formerly ForgeRock AS)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # `ping-lf` — Ping Login Framework CLI
2
+
3
+ CLI for initializing, scaffolding, and updating Ping Login Widget and Login App custom component projects.
4
+
5
+ ## Prerequisites
6
+
7
+ - **Node.js** >= 20.0.0
8
+ - **pnpm** >= 9
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm install -g @forgerock/login-framework-cli
14
+ ```
15
+
16
+ Or run directly without installing:
17
+
18
+ ```sh
19
+ npx @forgerock/login-framework-cli init my-project
20
+ ```
21
+
22
+ ## Commands
23
+
24
+ ### `ping-lf init <directory>`
25
+
26
+ Bootstraps a new Login Widget or Login App project into `<directory>` by fetching the latest framework release from GitHub, copying it with safe exclusions, and setting up the custom component scaffolding.
27
+
28
+ ```sh
29
+ ping-lf init my-login-project
30
+ cd my-login-project
31
+ pnpm install
32
+ cp .env.example .env # fill in your ForgeRock AM details
33
+ pnpm dev
34
+ ```
35
+
36
+ **Options**
37
+
38
+ | Flag | Description |
39
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
40
+ | `--version <tag>` | Download a specific release (e.g. `v1.2.0`). Defaults to latest. |
41
+ | `--local <path>` | Use a local framework directory instead of downloading from GitHub. Useful for development or air-gapped environments. |
42
+
43
+ **What it does**
44
+
45
+ 1. Downloads the framework archive from GitHub Releases (or copies from `--local`)
46
+ 2. Copies framework files, excluding build artifacts, secrets, and `tools/`
47
+ 3. Writes a minimal `pnpm-workspace.yaml`
48
+ 4. Creates `experimental/custom/callbacks/` and `experimental/custom/stages/` with `.gitkeep` placeholders
49
+ 5. Copies the component authoring guide to `experimental/custom/README.md`
50
+ 6. Seeds an empty `core/journey/_utilities/custom-registry.ts`
51
+ 7. Injects the custom registry Vite plugin into widget and app configs
52
+ 8. Writes `.generator-version` to track the framework version in use
53
+
54
+ ---
55
+
56
+ ### `ping-lf generate callback <Name>`
57
+
58
+ ### `ping-lf generate stage <Name>`
59
+
60
+ Scaffolds a new custom component under `experimental/custom/callbacks/<slug>/` or `experimental/custom/stages/<slug>/`, then regenerates `custom-registry.ts`.
61
+
62
+ Run from the root of an initialized project.
63
+
64
+ ```sh
65
+ # Override the built-in NameCallback renderer
66
+ ping-lf generate callback NameCallback
67
+
68
+ # Override the DefaultLogin stage layout
69
+ ping-lf generate stage DefaultLogin
70
+
71
+ # Create a brand-new callback type (paired with a custom AM node)
72
+ ping-lf generate callback MyTelemetryCallback
73
+ ```
74
+
75
+ **What it creates** (example for `ping-lf generate callback NameCallback`):
76
+
77
+ ```
78
+ experimental/custom/callbacks/name-callback/
79
+ ├── name-callback.svelte # component implementation — edit this
80
+ └── name-callback.utilities.ts # helper functions (optional)
81
+ └── name-callback.utilities.test.ts # unit test stub
82
+ ```
83
+
84
+ The `@component` header in the generated `.svelte` file declares the type and name. The pre-build script and `ping-lf` both read this header to register the component:
85
+
86
+ ```svelte
87
+ <!--
88
+ @component
89
+ Type: callback
90
+ Name: NameCallback
91
+ -->
92
+ ```
93
+
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
+
96
+ See `experimental/custom/README.md` for the full prop contract.
97
+
98
+ ---
99
+
100
+ ### `ping-lf update`
101
+
102
+ Fetches the latest (or a specified) framework version and overwrites the core framework files while **preserving** `experimental/custom/` and project-level configs. Regenerates the component registry after the update.
103
+
104
+ Run from the root of an initialized project.
105
+
106
+ ```sh
107
+ ping-lf update # update to latest
108
+ ping-lf update --version v1.5.0 # pin to a specific version
109
+ ping-lf update --local ../framework # use a local directory
110
+ ```
111
+
112
+ **What it preserves**
113
+
114
+ - `experimental/custom/` — all custom components are untouched
115
+ - `pnpm-workspace.yaml` — your workspace config is not overwritten
116
+ - `.env` files — excluded from the copy
117
+
118
+ **After updating**, run `pnpm install` if the framework's package dependencies changed.
119
+
120
+ ---
121
+
122
+ ## Project layout (after `init`)
123
+
124
+ ```
125
+ my-login-project/
126
+ ├── packages/login-widget/ # @forgerock/login-widget
127
+ ├── apps/login-app/ # SvelteKit dev/docs app
128
+ ├── core/ # Shared stores, components, journey logic
129
+ │ └── journey/_utilities/
130
+ │ └── custom-registry.ts # AUTO-GENERATED — do not edit
131
+ ├── experimental/
132
+ │ └── custom/
133
+ │ ├── README.md # Component authoring guide
134
+ │ ├── callbacks/ # Your custom callback components
135
+ │ └── stages/ # Your custom stage components
136
+ ├── .generator-version # Tracks the framework version in use
137
+ └── pnpm-workspace.yaml
138
+ ```
139
+
140
+ ## Development
141
+
142
+ > **All commands below must be run from the `tools/cli/` directory.**
143
+
144
+ ```sh
145
+ cd tools/cli
146
+
147
+ # Install dependencies
148
+ pnpm install
149
+
150
+ # Build the CLI
151
+ pnpm build
152
+
153
+ # Run tests
154
+ pnpm test
155
+
156
+ # Watch mode (rebuild on file changes)
157
+ pnpm dev
158
+ ```
159
+
160
+ ### Running locally without installing globally
161
+
162
+ ```sh
163
+ # From tools/cli/ after pnpm build:
164
+ node dist/main.js init my-project
165
+
166
+ # Or from anywhere using the full path:
167
+ node /path/to/forgerock-web-login-framework/tools/cli/dist/main.js init my-project
168
+ ```
169
+
170
+ ### Project structure
171
+
172
+ ```
173
+ tools/cli/
174
+ ├── src/
175
+ │ ├── main.ts # CLI entry point (ping-lf binary)
176
+ │ ├── errors.ts # Typed Effect errors
177
+ │ ├── commands/
178
+ │ │ ├── init.ts # ping-lf init
179
+ │ │ ├── generate.ts # ping-lf generate callback|stage
180
+ │ │ └── update.ts # ping-lf update
181
+ │ ├── services/
182
+ │ │ ├── file-system.ts # copyWithExclusions
183
+ │ │ ├── registry.ts # component scanning + registry generation
184
+ │ │ ├── release.ts # GitHub release fetch
185
+ │ │ └── vite-config.ts # Vite plugin injection
186
+ │ └── config/
187
+ │ ├── exclusions.ts # Files/dirs excluded from copy operations
188
+ │ └── version.ts # .generator-version read/write
189
+ ├── templates/
190
+ │ ├── callback/ # Scaffold templates for callbacks
191
+ │ └── stage/ # Scaffold templates for stages
192
+ └── test/ # Vitest unit tests
193
+ ```
194
+
195
+ ### Tech stack
196
+
197
+ - **[Effect](https://effect.website/)** — typed async handling, composable services, declarative errors
198
+ - **[@effect/cli](https://github.com/Effect-TS/effect/tree/main/packages/cli)** — CLI parsing and help generation
199
+ - **[@effect/platform-node](https://github.com/Effect-TS/effect/tree/main/packages/platform-node)** — Node.js FileSystem, Path, HTTP client
200
+ - **[Vitest](https://vitest.dev/)** — unit tests
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Thin Effect runner for generating the custom component registry.
4
+ *
5
+ * Called by:
6
+ * - packages/login-widget/package.json "prebuild"
7
+ * - Root package.json "generate:registry"
8
+ *
9
+ * Requires the CLI to be compiled first:
10
+ * pnpm --filter @forgerock/login-framework-cli run build
11
+ */
12
+ import { Effect } from 'effect';
13
+ import { NodeContext, NodeRuntime } from '@effect/platform-node';
14
+ const { runRegistryScript } = await import('../src/services/registry.js');
15
+ const projectDir = process.argv[2];
16
+ if (!projectDir) {
17
+ console.error('Usage: node generate-registry.mjs <projectDir>');
18
+ process.exit(1);
19
+ }
20
+ NodeRuntime.runMain(runRegistryScript(projectDir).pipe(Effect.provide(NodeContext.layer)));
@@ -0,0 +1,28 @@
1
+ import { Command } from '@effect/cli';
2
+ import { FileSystem, Path } from '@effect/platform';
3
+ import { Schema } from 'effect';
4
+ import { ComponentAlreadyExistsError, InvalidComponentNameError } from '../errors.js';
5
+ /**
6
+ * Validates a callback component name: must be a valid name (PascalCase, alphanumeric only).
7
+ * Callback names are driven by AM's callback type strings (e.g. NameCallback, PasswordCallback).
8
+ */
9
+ export declare const CallbackNameSchema: Schema.transform<Schema.filter<typeof Schema.String>, Schema.Struct<{
10
+ name: typeof Schema.String;
11
+ slug: typeof Schema.String;
12
+ }>>;
13
+ /**
14
+ * Validates a stage component name: accepts any string that is safe for a directory name and
15
+ * HTML comment embedding. Stage names in AM are arbitrary strings set on the Page Node — there
16
+ * is no name constraint, unlike callbacks.
17
+ */
18
+ export declare const StageNameSchema: Schema.transform<Schema.filter<typeof Schema.String>, Schema.Struct<{
19
+ name: typeof Schema.String;
20
+ slug: typeof Schema.String;
21
+ }>>;
22
+ export declare const generateCommand: Command.Command<"generate", FileSystem.FileSystem | Path.Path | import("@effect/platform/Terminal").Terminal, import("../errors.js").GeneratorVersionError | import("../errors.js").RegistryScanError | InvalidComponentNameError | ComponentAlreadyExistsError | import("@effect/platform/Error").PlatformError | import("@effect/platform/Terminal").QuitException, {
23
+ readonly subcommand: import("effect/Option").Option<{
24
+ readonly name: string;
25
+ } | {
26
+ readonly name: string;
27
+ }>;
28
+ }>;
@@ -0,0 +1,137 @@
1
+ import { Args, Command, Prompt } from '@effect/cli';
2
+ import { FileSystem, Path } from '@effect/platform';
3
+ import { Console, Effect, Schema } from 'effect';
4
+ import { fileURLToPath } from 'node:url';
5
+ import nodePath from 'node:path';
6
+ import { runRegistryScript } from '../services/registry.js';
7
+ import { assertValidProject } from '../config/version.js';
8
+ import { ComponentAlreadyExistsError, InvalidComponentNameError } from '../errors.js';
9
+ const CUSTOM_DIR = 'experimental/custom';
10
+ /**
11
+ * Converts a PascalCase name to a kebab-case slug.
12
+ * Uses two passes to correctly handle acronyms.
13
+ * Examples: "MyCallback" → "my-callback", "JWTLogin" → "jwt-login", "MyURLCallback" → "my-url-callback"
14
+ */
15
+ function toKebabCase(name) {
16
+ return name
17
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
18
+ .replace(/([a-z\d])([A-Z])/g, '$1-$2')
19
+ .toLowerCase();
20
+ }
21
+ /**
22
+ * Converts an arbitrary string to a kebab-case directory slug.
23
+ * Used for stage names which may contain spaces, hyphens, or mixed case.
24
+ * Examples: "My Login Stage" → "my-login-stage", "DefaultLogin" → "defaultlogin", "OTP Login" → "otp-login"
25
+ */
26
+ function toSlug(name) {
27
+ return name
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, '-')
30
+ .replace(/^-+|-+$/g, '');
31
+ }
32
+ /**
33
+ * Validates a callback component name: must be a valid name (PascalCase, alphanumeric only).
34
+ * Callback names are driven by AM's callback type strings (e.g. NameCallback, PasswordCallback).
35
+ */
36
+ export const CallbackNameSchema = Schema.String.pipe(Schema.filter((s) => /^[A-Z][A-Za-z0-9]+$/.test(s)), Schema.transform(Schema.Struct({ name: Schema.String, slug: Schema.String }), {
37
+ strict: true,
38
+ decode: (name) => ({ name, slug: toKebabCase(name) }),
39
+ encode: ({ name }) => name,
40
+ }));
41
+ /**
42
+ * Validates a stage component name: accepts any string that is safe for a directory name and
43
+ * HTML comment embedding. Stage names in AM are arbitrary strings set on the Page Node — there
44
+ * is no name constraint, unlike callbacks.
45
+ */
46
+ export const StageNameSchema = Schema.String.pipe(Schema.filter((s) => s.length >= 2 &&
47
+ /[A-Za-z]/.test(s) &&
48
+ !s.includes('\0') &&
49
+ !/[\n\r]/.test(s) &&
50
+ !/^\.\.?[/\\]/.test(s)), Schema.transform(Schema.Struct({ name: Schema.String, slug: Schema.String }), {
51
+ strict: true,
52
+ decode: (name) => ({ name, slug: toSlug(name) }),
53
+ encode: ({ name }) => name,
54
+ }));
55
+ /**
56
+ * Resolves the templates directory relative to this compiled file.
57
+ * Build script copies templates/ → dist/templates/, so from dist/commands/
58
+ * the templates are one level up at dist/templates/.
59
+ */
60
+ function getTemplatesDir() {
61
+ const __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
62
+ return nodePath.join(__dirname, '../templates');
63
+ }
64
+ function scaffoldComponent(type, name) {
65
+ return Effect.gen(function* () {
66
+ const fs = yield* FileSystem.FileSystem;
67
+ const p = yield* Path.Path;
68
+ // ── Guard: validate component name format ─────────────────────────────
69
+ // Callbacks must be valid name (PascalCase, alphanumeric only).
70
+ // Stages are arbitrary AM strings with no naming convention constraint.
71
+ const nameSchema = type === 'callback' ? CallbackNameSchema : StageNameSchema;
72
+ const { slug } = yield* Schema.decode(nameSchema)(name).pipe(Effect.mapError(() => new InvalidComponentNameError({ name })));
73
+ const cwd = process.cwd();
74
+ // ── Guard: must be run from an initialized project root ───────────────
75
+ yield* assertValidProject(cwd);
76
+ const subDir = type === 'callback' ? 'callbacks' : 'stages';
77
+ const componentDir = p.join(cwd, CUSTOM_DIR, subDir, slug);
78
+ const templatesDir = p.join(getTemplatesDir(), type);
79
+ // ── Guard: prevent overwriting an existing component ──────────────────
80
+ const exists = yield* fs.exists(componentDir);
81
+ if (exists) {
82
+ yield* Effect.fail(new ComponentAlreadyExistsError({ path: componentDir }));
83
+ }
84
+ yield* Console.log(`\nGenerating ${type} component: ${name}\n`);
85
+ // ── Create component directory ─────────────────────────────────────────
86
+ yield* fs.makeDirectory(componentDir, { recursive: true });
87
+ // ── Copy and process template files ───────────────────────────────────
88
+ // Each file name contains __COMPONENT_SLUG__; its contents contain both
89
+ // __COMPONENT_NAME__ (PascalCase) and __COMPONENT_SLUG__ (kebab-case).
90
+ const templateFiles = yield* fs.readDirectory(templatesDir);
91
+ const createdFiles = [];
92
+ for (const templateFile of templateFiles) {
93
+ const targetFileName = templateFile.replaceAll('__COMPONENT_SLUG__', slug);
94
+ const sourceContent = yield* fs.readFileString(p.join(templatesDir, templateFile));
95
+ const processedContent = sourceContent
96
+ .replaceAll('__COMPONENT_NAME__', name)
97
+ .replaceAll('__COMPONENT_SLUG__', slug);
98
+ const targetPath = p.join(componentDir, targetFileName);
99
+ yield* fs.writeFileString(targetPath, processedContent);
100
+ createdFiles.push(targetPath);
101
+ }
102
+ // ── Regenerate custom-registry.ts ────────────────────────────────────
103
+ yield* Console.log('Regenerating custom component registry...');
104
+ yield* runRegistryScript(cwd);
105
+ // ── Print summary ──────────────────────────────────────────────────────
106
+ yield* Console.log(`Done. ${type} component scaffolded successfully.\n\n` +
107
+ `Files created:\n` +
108
+ createdFiles.map((f) => ` ${f}`).join('\n') +
109
+ `\n\nNext: open ${p.join(componentDir, `${slug}.svelte`)} and implement your component.\n`);
110
+ });
111
+ }
112
+ // ── Subcommands ──────────────────────────────────────────────────────────────
113
+ const generateCallbackCommand = Command.make('callback', {
114
+ name: Args.text({ name: 'Name' }).pipe(Args.withDescription('PascalCase component name (e.g. MyCallback). Must match the callback type string your AM node sends.')),
115
+ }, ({ name }) => scaffoldComponent('callback', name)).pipe(Command.withDescription('Scaffold a new custom callback component under experimental/custom/callbacks/.'));
116
+ const generateStageCommand = Command.make('stage', {
117
+ name: Args.text({ name: 'Name' }).pipe(Args.withDescription('Stage name as configured on your AM journey Page Node (e.g. "DefaultLogin" or "My Login Stage"). Any non-empty string is accepted — stage names in AM are arbitrary and not constrained to PascalCase.')),
118
+ }, ({ name }) => scaffoldComponent('stage', name)).pipe(Command.withDescription('Scaffold a new custom stage component under experimental/custom/stages/.'));
119
+ export const generateCommand = Command.make('generate', {}, () => Effect.gen(function* () {
120
+ const type = yield* Prompt.select({
121
+ message: 'Select component type',
122
+ choices: [
123
+ {
124
+ title: 'callback',
125
+ value: 'callback',
126
+ description: 'Custom AM callback renderer',
127
+ },
128
+ { title: 'stage', value: 'stage', description: 'Custom journey stage layout' },
129
+ ],
130
+ });
131
+ const name = yield* Prompt.text({
132
+ message: type === 'callback'
133
+ ? 'Callback name (PascalCase name, e.g. MyCallback)'
134
+ : 'Stage name (as set on your AM Page Node, e.g. DefaultLogin or My Login Stage)',
135
+ });
136
+ yield* scaffoldComponent(type, name);
137
+ })).pipe(Command.withDescription('Scaffold a new custom callback or stage component from a template.'), Command.withSubcommands([generateCallbackCommand, generateStageCommand]));
@@ -0,0 +1,8 @@
1
+ import { Command } from '@effect/cli';
2
+ import { FileSystem, Path } from '@effect/platform';
3
+ import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
4
+ 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 | import("../errors.js").RegistryScanError | DirectoryConflictError | DirectoryNotEmptyError | import("@effect/platform/Error").PlatformError, {
5
+ readonly directory: string;
6
+ readonly local: import("effect/Option").Option<string>;
7
+ readonly version: import("effect/Option").Option<string>;
8
+ }>;
@@ -0,0 +1,95 @@
1
+ import { Args, Command, Options } from '@effect/cli';
2
+ import { FileSystem, Path } from '@effect/platform';
3
+ import { Console, Effect } from 'effect';
4
+ import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
5
+ import { writeVersion } from '../config/version.js';
6
+ import { copyWithExclusions, expandTilde, isFrameworkDirectory } from '../services/file-system.js';
7
+ import { runRegistryScript } from '../services/registry.js';
8
+ import { resolveSource } from './source.js';
9
+ /**
10
+ * Minimal pnpm-workspace.yaml written to the customer project.
11
+ * Intentionally omits `tools/` — that workspace entry exists only in the
12
+ * upstream monorepo and is not needed (or valid) in a customer project.
13
+ */
14
+ const PNPM_WORKSPACE = `packages:
15
+ - 'packages/*'
16
+ - 'apps/*'
17
+ - 'e2e'
18
+ `;
19
+ const nextStepsMessage = (dir) => `
20
+ Done. Project initialized successfully.
21
+
22
+ Next steps:
23
+ cd ${dir}
24
+ pnpm install
25
+ cp .env.example .env # add your ForgeRock AM connection details
26
+ pnpm dev
27
+
28
+ To scaffold your first custom component:
29
+ ping-lf generate callback MyCallback
30
+ ping-lf generate stage MyStage
31
+
32
+ Read the authoring guide: experimental/custom/README.md
33
+ `;
34
+ export const initCommand = Command.make('init', {
35
+ directory: Args.text({ name: 'directory' }).pipe(Args.withDescription('Directory to initialize the project in. Use "./" for the current directory.')),
36
+ local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
37
+ version: Options.optional(Options.text('version').pipe(Options.withDescription('Framework version tag to download (e.g. v1.2.0). If omitted, the main branch is used.'))),
38
+ }, ({ directory, local, version }) => Effect.gen(function* () {
39
+ const fs = yield* FileSystem.FileSystem;
40
+ const path = yield* Path.Path;
41
+ const resolvedDir = path.resolve(expandTilde(directory));
42
+ yield* Console.log(`\nInitializing Ping Login Widget / Login App project in: ${resolvedDir}\n`);
43
+ // ── 0. Validate target directory ──────────────────────────────────────
44
+ const dirExists = yield* fs.exists(resolvedDir);
45
+ if (dirExists) {
46
+ const isFramework = yield* isFrameworkDirectory(resolvedDir);
47
+ if (isFramework) {
48
+ yield* Effect.fail(new DirectoryConflictError({ path: resolvedDir }));
49
+ }
50
+ const entries = yield* fs
51
+ .readDirectory(resolvedDir)
52
+ .pipe(Effect.orElseSucceed(() => []));
53
+ if (entries.length > 0) {
54
+ yield* Effect.fail(new DirectoryNotEmptyError({ path: resolvedDir }));
55
+ }
56
+ }
57
+ // ── 1. Resolve source + copy framework (scoped: temp dir auto-cleaned) ──
58
+ // Effect.scoped closes the Scope that release.fetch opens, triggering
59
+ // automatic removal of the .framework-tmp directory after copying.
60
+ const resolvedVersion = yield* Effect.scoped(Effect.gen(function* () {
61
+ const { sourceDir, resolvedVersion } = yield* resolveSource(local, version, resolvedDir);
62
+ // ── 2. Create target directory + copy framework (with exclusions) ──
63
+ yield* Console.log('Copying framework files...');
64
+ yield* fs.makeDirectory(resolvedDir, { recursive: true });
65
+ yield* copyWithExclusions(sourceDir, resolvedDir);
66
+ return resolvedVersion;
67
+ }));
68
+ // ── 3. Write pnpm-workspace.yaml (omits tools/ present in upstream) ────
69
+ yield* fs.writeFileString(path.join(resolvedDir, 'pnpm-workspace.yaml'), PNPM_WORKSPACE);
70
+ // ── 4. Scaffold experimental/custom/ ──────────────────────────────────
71
+ // copyWithExclusions already copies experimental/custom/demo/,
72
+ // login-framework.ts, README.md, and tsconfig.json from the source.
73
+ // Only callbacks/ and stages/ are protected (they hold user components).
74
+ // Create them as empty dirs with .gitkeep — components come via `ping-lf generate`.
75
+ yield* Console.log('Scaffolding custom component directories...');
76
+ yield* Effect.forEach(['callbacks', 'stages'], (dir) => Effect.gen(function* () {
77
+ const customDir = path.join(resolvedDir, 'experimental', 'custom', dir);
78
+ yield* fs.makeDirectory(customDir, { recursive: true });
79
+ yield* fs.writeFileString(path.join(customDir, '.gitkeep'), '');
80
+ }), { concurrency: 'unbounded', discard: true });
81
+ // ── 5. Generate custom-registry.ts ────────────────────────────────────
82
+ // Run the canonical registry script so the file is immediately present
83
+ // with the correct CustomRegistryEntry format. The empty callbacks/ and
84
+ // stages/ dirs produce empty registries — no custom components yet.
85
+ yield* Console.log('Generating custom component registry...');
86
+ yield* runRegistryScript(resolvedDir);
87
+ // ── 6. Write .generator-version ────────────────────────────────────────
88
+ yield* writeVersion(resolvedDir, {
89
+ version: resolvedVersion,
90
+ commitHash: '',
91
+ generatedAt: new Date().toISOString(),
92
+ });
93
+ // ── 7. Print next steps ───────────────────────────────────────────────
94
+ yield* Console.log(nextStepsMessage(directory));
95
+ })).pipe(Command.withDescription('Bootstrap a new Ping Login Widget or Login App project from a GitHub release or a local framework path.'));
@@ -0,0 +1,3 @@
1
+ import { Command } from '@effect/cli';
2
+ import { Release } from '../services/release.js';
3
+ export declare const releasesCommand: Command.Command<"releases", Release, import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseNotFoundError, {}>;
@@ -0,0 +1,12 @@
1
+ import { Command } from '@effect/cli';
2
+ import { Console, Effect } from 'effect';
3
+ import { Release } from '../services/release.js';
4
+ export const releasesCommand = Command.make('releases', {}, () => Effect.gen(function* () {
5
+ const release = yield* Release;
6
+ const releases = yield* release.listReleases();
7
+ yield* Console.log('\nAvailable Login Framework releases:\n');
8
+ for (const { tag, publishedAt } of releases) {
9
+ yield* Console.log(` ${tag.padEnd(10)} ${publishedAt}`);
10
+ }
11
+ yield* Console.log('');
12
+ })).pipe(Command.withDescription('List available Login Framework releases from GitHub.'));
@@ -0,0 +1,22 @@
1
+ import { Path } from '@effect/platform';
2
+ import { Effect, Option } from 'effect';
3
+ import { Release } from '../services/release.js';
4
+ /**
5
+ * Resolves the framework source directory and version from a local path or a
6
+ * GitHub release download. Shared by `init` and `update` commands.
7
+ *
8
+ * @param local - Optional local framework directory path.
9
+ * @param version - Optional explicit release version tag.
10
+ * @param targetDir - Directory that will receive the downloaded release temp
11
+ * folder (`.framework-tmp`).
12
+ */
13
+ export declare const resolveSource: (local: Option.Option<string>, version: Option.Option<string>, targetDir: string) => Effect.Effect<{
14
+ sourceDir: string;
15
+ resolvedVersion: "main";
16
+ } | {
17
+ sourceDir: string;
18
+ resolvedVersion: string;
19
+ } | {
20
+ sourceDir: string;
21
+ resolvedVersion: "local";
22
+ }, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError, import("@effect/platform/FileSystem").FileSystem | Path.Path | Release | import("effect/Scope").Scope>;
@@ -0,0 +1,24 @@
1
+ import { Path } from '@effect/platform';
2
+ import { Console, Effect, Option, pipe } from 'effect';
3
+ import { expandTilde } from '../services/file-system.js';
4
+ import { Release } from '../services/release.js';
5
+ /**
6
+ * Resolves the framework source directory and version from a local path or a
7
+ * GitHub release download. Shared by `init` and `update` commands.
8
+ *
9
+ * @param local - Optional local framework directory path.
10
+ * @param version - Optional explicit release version tag.
11
+ * @param targetDir - Directory that will receive the downloaded release temp
12
+ * folder (`.framework-tmp`).
13
+ */
14
+ export const resolveSource = (local, version, targetDir) => Effect.gen(function* () {
15
+ const path = yield* Path.Path;
16
+ const release = yield* Release;
17
+ return yield* Option.match(local, {
18
+ onSome: (localPath) => pipe(Effect.sync(() => path.resolve(expandTilde(localPath))), Effect.tap((dir) => Console.log(`Using local source: ${dir}`)), Effect.map((dir) => ({ sourceDir: dir, resolvedVersion: 'local' }))),
19
+ onNone: () => pipe(Option.match(version, {
20
+ onSome: (v) => pipe(Effect.tap(Effect.succeed(v), (v) => Console.log(`Fetching framework version: ${v}...`)), Effect.flatMap((v) => pipe(release.fetch(v, targetDir), Effect.map((sourceDir) => ({ sourceDir, resolvedVersion: v }))))),
21
+ onNone: () => pipe(Console.log('Fetching framework from main branch...'), Effect.flatMap(() => release.fetchBranch('main', targetDir)), Effect.map((sourceDir) => ({ sourceDir, resolvedVersion: 'main' }))),
22
+ })),
23
+ });
24
+ });
@@ -0,0 +1,5 @@
1
+ import { Command } from '@effect/cli';
2
+ 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, {
3
+ readonly local: import("effect/Option").Option<string>;
4
+ readonly version: import("effect/Option").Option<string>;
5
+ }>;
@@ -0,0 +1,36 @@
1
+ import { Command, Options } from '@effect/cli';
2
+ import { Console, Effect } from 'effect';
3
+ import { writeVersion, assertValidProject } from '../config/version.js';
4
+ import { copyWithExclusions } from '../services/file-system.js';
5
+ import { runRegistryScript } from '../services/registry.js';
6
+ import { resolveSource } from './source.js';
7
+ export const updateCommand = Command.make('update', {
8
+ local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
9
+ version: Options.optional(Options.text('version').pipe(Options.withDescription('Framework version tag to update to (e.g. v1.2.0). If omitted, the main branch is used.'))),
10
+ }, ({ local, version }) => Effect.gen(function* () {
11
+ const cwd = process.cwd();
12
+ // ── 1. Verify this is an initialized project ──────────────────────────
13
+ const currentVersion = yield* assertValidProject(cwd);
14
+ yield* Console.log(`\nUpdating project from version: ${currentVersion.version}\n`);
15
+ // ── 2. Resolve source + copy framework (scoped: temp dir auto-cleaned) ──
16
+ // Effect.scoped closes the Scope that release.fetch opens, triggering
17
+ // automatic removal of the .framework-tmp directory after copying.
18
+ const resolvedVersion = yield* Effect.scoped(Effect.gen(function* () {
19
+ const { sourceDir, resolvedVersion } = yield* resolveSource(local, version, cwd);
20
+ yield* Console.log('Copying updated framework files...');
21
+ yield* copyWithExclusions(sourceDir, cwd);
22
+ return resolvedVersion;
23
+ }));
24
+ // ── 3. Regenerate custom-registry.ts ──────────────────────────────────
25
+ yield* Console.log('Regenerating custom component registry...');
26
+ yield* runRegistryScript(cwd);
27
+ // ── 4. Update .generator-version ──────────────────────────────────────
28
+ yield* writeVersion(cwd, {
29
+ version: resolvedVersion,
30
+ commitHash: '',
31
+ generatedAt: new Date().toISOString(),
32
+ });
33
+ yield* Console.log(`\nDone. Updated from ${currentVersion.version} to ${resolvedVersion}.\n` +
34
+ 'Custom component registry regenerated.\n' +
35
+ 'Run "pnpm install" if package dependencies changed.\n');
36
+ })).pipe(Command.withDescription('Fetch the latest (or a specified) framework version and overwrite core files while preserving experimental/custom/.'));
@@ -0,0 +1 @@
1
+ export declare function isExcluded(relativePath: string): boolean;