@forgerock/login-framework-cli 0.0.0-beta.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +107 -34
  2. package/dist/{commands → src/commands}/generate.d.ts +3 -2
  3. package/dist/{commands → src/commands}/generate.js +15 -8
  4. package/dist/src/commands/init.d.ts +16 -0
  5. package/dist/{commands → src/commands}/init.js +8 -10
  6. package/dist/{commands → src/commands}/source.d.ts +1 -1
  7. package/dist/{commands → src/commands}/update.d.ts +4 -2
  8. package/dist/{commands → src/commands}/update.js +9 -8
  9. package/dist/{config → src/config}/version.d.ts +1 -4
  10. package/dist/{config → src/config}/version.js +1 -2
  11. package/dist/src/main.js +61 -0
  12. package/dist/src/mcp.d.ts +52 -0
  13. package/dist/src/mcp.js +182 -0
  14. package/dist/{services → src/services}/registry.d.ts +9 -2
  15. package/dist/{services → src/services}/registry.js +20 -13
  16. package/dist/{services → src/services}/release.d.ts +2 -1
  17. package/dist/{services → src/services}/release.js +2 -2
  18. package/dist/src/templates/callback/__COMPONENT_SLUG__.mock.ts +22 -0
  19. package/dist/src/templates/callback/__COMPONENT_SLUG__.stories.js +28 -0
  20. package/dist/src/templates/callback/__COMPONENT_SLUG__.story.svelte +59 -0
  21. package/dist/src/templates/callback/__COMPONENT_SLUG__.svelte +49 -0
  22. package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +9 -0
  23. package/dist/{templates → src/templates}/callback/__COMPONENT_SLUG__.utilities.ts +1 -1
  24. package/dist/src/templates/stage/__COMPONENT_SLUG__.mock.ts +23 -0
  25. package/dist/src/templates/stage/__COMPONENT_SLUG__.stories.js +43 -0
  26. package/dist/src/templates/stage/__COMPONENT_SLUG__.story.svelte +47 -0
  27. package/dist/{templates → src/templates}/stage/__COMPONENT_SLUG__.svelte +21 -19
  28. package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +9 -0
  29. package/dist/{templates → src/templates}/stage/__COMPONENT_SLUG__.utilities.ts +1 -1
  30. package/dist/src/templates/tsconfig.json +18 -0
  31. package/dist/src/utils.d.ts +11 -0
  32. package/dist/src/utils.js +17 -0
  33. package/package.json +13 -10
  34. package/dist/commands/init.d.ts +0 -8
  35. package/dist/main.js +0 -34
  36. package/dist/templates/callback/__COMPONENT_SLUG__.svelte +0 -73
  37. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +0 -9
  38. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +0 -9
  39. package/dist/utils.d.ts +0 -2
  40. package/dist/utils.js +0 -4
  41. package/scripts/generate-registry.mjs +0 -23
  42. package/dist/{commands → src/commands}/releases.d.ts +0 -0
  43. package/dist/{commands → src/commands}/releases.js +0 -0
  44. package/dist/{commands → src/commands}/source.js +0 -0
  45. package/dist/{config → src/config}/exclusions.d.ts +0 -0
  46. package/dist/{config → src/config}/exclusions.js +0 -0
  47. package/dist/{errors.d.ts → src/errors.d.ts} +8 -8
  48. package/dist/{errors.js → src/errors.js} +2 -2
  49. package/dist/{main.d.ts → src/main.d.ts} +0 -0
  50. package/dist/{services → src/services}/file-system.d.ts +1 -1
  51. package/dist/{services → src/services}/file-system.js +2 -2
package/README.md CHANGED
@@ -10,13 +10,13 @@ CLI for initializing, scaffolding, and updating Ping Login Widget and Login App
10
10
  ## Installation
11
11
 
12
12
  ```sh
13
- npm install -g @ping-identity/login-framework-cli
13
+ npm install -g @forgerock/login-framework-cli
14
14
  ```
15
15
 
16
16
  Or run directly without installing:
17
17
 
18
18
  ```sh
19
- npx @ping-identity/login-framework-cli init my-project
19
+ npx @forgerock/login-framework-cli init my-project
20
20
  ```
21
21
 
22
22
  ## Commands
@@ -35,10 +35,10 @@ pnpm dev
35
35
 
36
36
  **Options**
37
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. |
38
+ | Flag | Description |
39
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
40
+ | `--tag <tag>` | Download a specific Git release tag (e.g. `@forgerock/login-widget@2.1.0`; legacy `v1.2.0` also works). Defaults to `main`. |
41
+ | `--local <path>` | Use a local framework directory instead of downloading from GitHub. Useful for development or air-gapped environments. |
42
42
 
43
43
  **What it does**
44
44
 
@@ -47,9 +47,9 @@ pnpm dev
47
47
  3. Writes a minimal `pnpm-workspace.yaml`
48
48
  4. Creates `experimental/custom/callbacks/` and `experimental/custom/stages/` with `.gitkeep` placeholders
49
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
50
+ 6. Writes `.generator-version` to track the framework version in use
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.
53
53
 
54
54
  ---
55
55
 
@@ -57,9 +57,9 @@ pnpm dev
57
57
 
58
58
  ### `ping-lf generate stage <Name>`
59
59
 
60
- Scaffolds a new custom component under `experimental/custom/callbacks/<slug>/` or `experimental/custom/stages/<slug>/`, then regenerates `custom-registry.ts`.
60
+ Scaffolds a new custom component under `experimental/custom/callbacks/<slug>/` or `experimental/custom/stages/<slug>/`. The Vite plugin watches these directories during `pnpm dev` and regenerates `custom-registry.ts` automatically on add/remove/change.
61
61
 
62
- Run from the root of an initialized project.
62
+ Run from the root of an initialized project, or pass `--directory <path>` to target a specific project root.
63
63
 
64
64
  ```sh
65
65
  # Override the built-in NameCallback renderer
@@ -81,7 +81,7 @@ experimental/custom/callbacks/name-callback/
81
81
  └── name-callback.utilities.test.ts # unit test stub
82
82
  ```
83
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:
84
+ The `@component` header in the generated `.svelte` file declares the type and name. The framework's Vite plugin reads this header to register the component:
85
85
 
86
86
  ```svelte
87
87
  <!--
@@ -97,16 +97,75 @@ See `experimental/custom/README.md` for the full prop contract.
97
97
 
98
98
  ---
99
99
 
100
+ ### `ping-lf releases`
101
+
102
+ Lists available framework releases from GitHub.
103
+
104
+ ```sh
105
+ ping-lf releases
106
+ ```
107
+
108
+ ---
109
+
110
+ ### `ping-lf mcp`
111
+
112
+ Boots the CLI as a local **MCP server** over stdio. Exposes the same commands as typed tools that any MCP-compatible AI assistant can call directly.
113
+
114
+ #### Claude Code (project-local)
115
+
116
+ Create `.claude/mcp.json` at the root of your login project:
117
+
118
+ ```jsonc
119
+ {
120
+ "mcpServers": {
121
+ "ping-lf": {
122
+ "command": "npx",
123
+ "args": ["@forgerock/login-framework-cli", "mcp"]
124
+ }
125
+ }
126
+ }
127
+ ```
128
+
129
+ #### Claude Desktop
130
+
131
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
132
+
133
+ ```jsonc
134
+ {
135
+ "mcpServers": {
136
+ "ping-lf": {
137
+ "command": "npx",
138
+ "args": ["@forgerock/login-framework-cli", "mcp"]
139
+ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ #### Available tools
145
+
146
+ | Tool | Description |
147
+ | ------------------- | ------------------------------------ |
148
+ | `init` | Bootstrap a new project |
149
+ | `generate_callback` | Scaffold a custom callback component |
150
+ | `generate_stage` | Scaffold a custom stage component |
151
+ | `update` | Update the framework version |
152
+ | `list_releases` | List available releases from GitHub |
153
+
154
+ For `generate_callback`, `generate_stage`, and `update`, pass an absolute path via the `directory` parameter to target a specific project root. If omitted, the server's working directory is used.
155
+
156
+ ---
157
+
100
158
  ### `ping-lf update`
101
159
 
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.
160
+ Fetches the latest (or a specified) framework version and overwrites the core framework files while **preserving** `experimental/custom/` and project-level configs. The component registry is regenerated by the Vite plugin on the next build or dev-server start.
103
161
 
104
162
  Run from the root of an initialized project.
105
163
 
106
164
  ```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
165
+ ping-lf update # update to latest
166
+ ping-lf update --tag @forgerock/login-widget@2.1.0 # pin to a specific release tag
167
+ ping-lf update --local ../framework # use a local directory
168
+ ping-lf update --directory /path/to/project # target a specific project root
110
169
  ```
111
170
 
112
171
  **What it preserves**
@@ -139,32 +198,45 @@ my-login-project/
139
198
 
140
199
  ## Development
141
200
 
142
- > **All commands below must be run from the `tools/cli/` directory.**
201
+ The CLI is a workspace package inside the framework monorepo, but it is **not** declared as a dependency of any other workspace. This is intentional: customer-generated projects must not carry a `workspace:*` link to this CLI, because that protocol only resolves inside this monorepo. The trade-off is that after a fresh clone, `ping-lf` is not on your `$PATH` until you set it up.
143
202
 
144
- ```sh
145
- cd tools/cli
203
+ ### Recommended setup (once per machine)
146
204
 
147
- # Install dependencies
148
- pnpm install
205
+ From the repo root, after `pnpm install`:
149
206
 
150
- # Build the CLI
151
- pnpm build
207
+ ```sh
208
+ pnpm --filter @forgerock/login-framework-cli build
209
+ pnpm --filter @forgerock/login-framework-cli link --global
210
+ ```
152
211
 
153
- # Run tests
154
- pnpm test
212
+ `pnpm link --global` symlinks the CLI's `bin` (`tools/cli/dist/src/main.js`) into your machine's global pnpm bin directory, pointing back at this repo's `dist/`. After this, `ping-lf` works as a real command from anywhere:
155
213
 
156
- # Watch mode (rebuild on file changes)
157
- pnpm dev
214
+ ```sh
215
+ ping-lf init my-project
216
+ ping-lf generate callback MyCallback
217
+ ping-lf update --tag @forgerock/login-widget@2.1.0
158
218
  ```
159
219
 
160
- ### Running locally without installing globally
220
+ Rebuilds (`pnpm --filter @forgerock/login-framework-cli build`) update the global binary automatically — no relink needed.
221
+
222
+ To remove the global link later: `pnpm --filter @forgerock/login-framework-cli unlink --global`.
223
+
224
+ ### Without the global link
225
+
226
+ If you'd rather not link globally, invoke the built binary directly. You still need to build first:
161
227
 
162
228
  ```sh
163
- # From tools/cli/ after pnpm build:
164
- node dist/main.js init my-project
229
+ pnpm --filter @forgerock/login-framework-cli build
230
+ ./tools/cli/dist/src/main.js init my-project
231
+ # or:
232
+ pnpm --filter @forgerock/login-framework-cli exec ping-lf init my-project
233
+ ```
165
234
 
166
- # Or from anywhere using the full path:
167
- node /path/to/forgerock-web-login-framework/tools/cli/dist/main.js init my-project
235
+ ### Other useful commands
236
+
237
+ ```sh
238
+ pnpm --filter @forgerock/login-framework-cli test # run unit tests
239
+ pnpm --filter @forgerock/login-framework-cli run dev # tsc --watch
168
240
  ```
169
241
 
170
242
  ### Project structure
@@ -172,7 +244,8 @@ node /path/to/forgerock-web-login-framework/tools/cli/dist/main.js init my-proje
172
244
  ```
173
245
  tools/cli/
174
246
  ├── src/
175
- │ ├── main.ts # CLI entry point (ping-lf binary)
247
+ │ ├── main.ts # Entry point forks to MCP server or CLI
248
+ │ ├── mcp.ts # MCP server (ping-lf --mcp)
176
249
  │ ├── errors.ts # Typed Effect errors
177
250
  │ ├── commands/
178
251
  │ │ ├── init.ts # ping-lf init
@@ -180,7 +253,6 @@ tools/cli/
180
253
  │ │ └── update.ts # ping-lf update
181
254
  │ ├── services/
182
255
  │ │ ├── file-system.ts # copyWithExclusions
183
- │ │ ├── registry.ts # component scanning + registry generation
184
256
  │ │ ├── release.ts # GitHub release fetch
185
257
  │ │ └── vite-config.ts # Vite plugin injection
186
258
  │ └── config/
@@ -196,5 +268,6 @@ tools/cli/
196
268
 
197
269
  - **[Effect](https://effect.website/)** — typed async handling, composable services, declarative errors
198
270
  - **[@effect/cli](https://github.com/Effect-TS/effect/tree/main/packages/cli)** — CLI parsing and help generation
271
+ - **[@effect/ai](https://github.com/Effect-TS/effect/tree/main/packages/ai)** — MCP server (`McpServer`, `Tool`, `Toolkit`)
199
272
  - **[@effect/platform-node](https://github.com/Effect-TS/effect/tree/main/packages/platform-node)** — Node.js FileSystem, Path, HTTP client
200
273
  - **[Vitest](https://vitest.dev/)** — unit tests
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@effect/cli';
2
2
  import { FileSystem, Path } from '@effect/platform';
3
- import { Schema } from 'effect';
3
+ import { Effect, Schema } from 'effect';
4
4
  import { ComponentAlreadyExistsError, InvalidComponentNameError } from '../errors.js';
5
5
  /**
6
6
  * Validates a callback component name: must be a valid name (PascalCase, alphanumeric only).
@@ -19,7 +19,8 @@ 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 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, {
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, {
23
24
  readonly subcommand: import("effect/Option").Option<{
24
25
  readonly name: string;
25
26
  } | {
@@ -1,11 +1,14 @@
1
1
  import { Args, Command, Prompt } from '@effect/cli';
2
2
  import { FileSystem, Path } from '@effect/platform';
3
3
  import { Console, Effect, Schema } from 'effect';
4
- import { fileURLToPath } from 'node:url';
4
+ import { existsSync } from 'node:fs';
5
5
  import nodePath from 'node:path';
6
- import { runRegistryScript } from '../services/registry.js';
6
+ import { fileURLToPath } from 'node:url';
7
7
  import { assertValidProject } from '../config/version.js';
8
8
  import { ComponentAlreadyExistsError, InvalidComponentNameError } from '../errors.js';
9
+ import { expandTilde } from '../services/file-system.js';
10
+ import { runRegistryScript } from '../services/registry.js';
11
+ import { toPascalCase } from '../utils.js';
9
12
  const CUSTOM_DIR = 'experimental/custom';
10
13
  /**
11
14
  * Converts a PascalCase name to a kebab-case slug.
@@ -53,15 +56,17 @@ export const StageNameSchema = Schema.String.pipe(Schema.filter((s) => s.length
53
56
  encode: ({ name }) => name,
54
57
  }));
55
58
  /**
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
+ * Resolves the templates directory relative to this file.
60
+ * In the compiled output (dist/src/commands/) the templates live one level up
61
+ * at dist/src/templates/. In Vitest (src/commands/) they live two levels up at
62
+ * the workspace root templates/ directory — the same fallback pattern used in mcp.ts.
59
63
  */
60
64
  function getTemplatesDir() {
61
65
  const __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
62
- return nodePath.join(__dirname, '../templates');
66
+ const compiledPath = nodePath.join(__dirname, '../templates');
67
+ return existsSync(compiledPath) ? compiledPath : nodePath.join(__dirname, '../../templates');
63
68
  }
64
- function scaffoldComponent(type, name) {
69
+ export function scaffoldComponent(type, name, directory) {
65
70
  return Effect.gen(function* () {
66
71
  const fs = yield* FileSystem.FileSystem;
67
72
  const p = yield* Path.Path;
@@ -70,7 +75,7 @@ function scaffoldComponent(type, name) {
70
75
  // Stages are arbitrary AM strings with no naming convention constraint.
71
76
  const nameSchema = type === 'callback' ? CallbackNameSchema : StageNameSchema;
72
77
  const { slug } = yield* Schema.decode(nameSchema)(name).pipe(Effect.mapError(() => new InvalidComponentNameError({ name })));
73
- const cwd = process.cwd();
78
+ const cwd = p.resolve(expandTilde(directory ?? process.cwd()));
74
79
  // ── Guard: must be run from an initialized project root ───────────────
75
80
  yield* assertValidProject(cwd);
76
81
  const subDir = type === 'callback' ? 'callbacks' : 'stages';
@@ -92,7 +97,9 @@ function scaffoldComponent(type, name) {
92
97
  for (const templateFile of templateFiles) {
93
98
  const targetFileName = templateFile.replaceAll('__COMPONENT_SLUG__', slug);
94
99
  const sourceContent = yield* fs.readFileString(p.join(templatesDir, templateFile));
100
+ const pascalName = toPascalCase(name);
95
101
  const processedContent = sourceContent
102
+ .replaceAll('__COMPONENT_NAME_PASCAL__', pascalName)
96
103
  .replaceAll('__COMPONENT_NAME__', name)
97
104
  .replaceAll('__COMPONENT_SLUG__', slug);
98
105
  const targetPath = p.join(componentDir, targetFileName);
@@ -0,0 +1,16 @@
1
+ import { Command } from '@effect/cli';
2
+ import { FileSystem, Path } from '@effect/platform';
3
+ import { Effect } from 'effect';
4
+ import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
5
+ import type { Option } from 'effect';
6
+ export interface InitProjectOptions {
7
+ readonly directory: string;
8
+ readonly local: Option.Option<string>;
9
+ readonly version: Option.Option<string>;
10
+ }
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, {
13
+ readonly directory: string;
14
+ readonly local: Option.Option<string>;
15
+ readonly tag: Option.Option<string>;
16
+ }>;
@@ -1,15 +1,13 @@
1
1
  import { Args, Command, Options } from '@effect/cli';
2
2
  import { FileSystem, Path } from '@effect/platform';
3
3
  import { Console, Effect } from 'effect';
4
- import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
5
4
  import { writeVersion } from '../config/version.js';
5
+ import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
6
6
  import { copyWithExclusions, expandTilde, isFrameworkDirectory } from '../services/file-system.js';
7
7
  import { runRegistryScript } from '../services/registry.js';
8
8
  import { resolveSource } from './source.js';
9
9
  /**
10
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
11
  */
14
12
  const PNPM_WORKSPACE = `packages:
15
13
  - 'packages/*'
@@ -31,11 +29,7 @@ To scaffold your first custom component:
31
29
 
32
30
  Read the authoring guide: experimental/custom/README.md
33
31
  `;
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* () {
32
+ export const initProject = ({ directory, local, version }) => Effect.gen(function* () {
39
33
  const fs = yield* FileSystem.FileSystem;
40
34
  const path = yield* Path.Path;
41
35
  const resolvedDir = path.resolve(expandTilde(directory));
@@ -87,9 +81,13 @@ export const initCommand = Command.make('init', {
87
81
  // ── 6. Write .generator-version ────────────────────────────────────────
88
82
  yield* writeVersion(resolvedDir, {
89
83
  version: resolvedVersion,
90
- commitHash: '',
91
84
  generatedAt: new Date().toISOString(),
92
85
  });
93
86
  // ── 7. Print next steps ───────────────────────────────────────────────
94
87
  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.'));
88
+ });
89
+ export const initCommand = Command.make('init', {
90
+ directory: Args.text({ name: 'directory' }).pipe(Args.withDescription('Directory to initialize the project in. Use "./" for the current directory.')),
91
+ local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
92
+ tag: Options.optional(Options.text('tag').pipe(Options.withDescription('Framework Git release tag to download (e.g. @forgerock/login-widget@2.1.0 or v1.2.0). If omitted, the main branch is used.'))),
93
+ }, ({ directory, local, tag }) => initProject({ directory, local, version: tag })).pipe(Command.withDescription('Bootstrap a new Ping Login Widget or Login App project from a GitHub release or a local framework path.'));
@@ -19,4 +19,4 @@ export declare const resolveSource: (local: Option.Option<string>, version: Opti
19
19
  } | {
20
20
  sourceDir: string;
21
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>;
22
+ }, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError, import("@effect/platform/FileSystem").FileSystem | Path.Path | import("effect/Scope").Scope | Release>;
@@ -1,5 +1,7 @@
1
1
  import { Command } from '@effect/cli';
2
+ import { Option } from 'effect';
2
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, {
3
- readonly local: import("effect/Option").Option<string>;
4
- readonly version: import("effect/Option").Option<string>;
4
+ readonly directory: Option.Option<string>;
5
+ readonly local: Option.Option<string>;
6
+ readonly tag: Option.Option<string>;
5
7
  }>;
@@ -1,14 +1,16 @@
1
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';
2
+ import { Console, Effect, Option } from 'effect';
3
+ import nodePath from 'node:path';
4
+ import { assertValidProject, writeVersion } from '../config/version.js';
5
+ import { copyWithExclusions, expandTilde } from '../services/file-system.js';
5
6
  import { runRegistryScript } from '../services/registry.js';
6
7
  import { resolveSource } from './source.js';
7
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.'))),
8
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.'))),
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();
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* () {
13
+ const cwd = nodePath.resolve(expandTilde(Option.getOrElse(directory, () => process.cwd())));
12
14
  // ── 1. Verify this is an initialized project ──────────────────────────
13
15
  const currentVersion = yield* assertValidProject(cwd);
14
16
  yield* Console.log(`\nUpdating project from version: ${currentVersion.version}\n`);
@@ -16,7 +18,7 @@ export const updateCommand = Command.make('update', {
16
18
  // Effect.scoped closes the Scope that release.fetch opens, triggering
17
19
  // automatic removal of the .framework-tmp directory after copying.
18
20
  const resolvedVersion = yield* Effect.scoped(Effect.gen(function* () {
19
- const { sourceDir, resolvedVersion } = yield* resolveSource(local, version, cwd);
21
+ const { sourceDir, resolvedVersion } = yield* resolveSource(local, tag, cwd);
20
22
  yield* Console.log('Copying updated framework files...');
21
23
  yield* copyWithExclusions(sourceDir, cwd);
22
24
  return resolvedVersion;
@@ -27,7 +29,6 @@ export const updateCommand = Command.make('update', {
27
29
  // ── 4. Update .generator-version ──────────────────────────────────────
28
30
  yield* writeVersion(cwd, {
29
31
  version: resolvedVersion,
30
- commitHash: '',
31
32
  generatedAt: new Date().toISOString(),
32
33
  });
33
34
  yield* Console.log(`\nDone. Updated from ${currentVersion.version} to ${resolvedVersion}.\n` +
@@ -1,15 +1,13 @@
1
- import { Effect, Schema } from 'effect';
2
1
  import { FileSystem } from '@effect/platform';
2
+ import { Effect, Schema } from 'effect';
3
3
  import { GeneratorVersionError } from '../errors.js';
4
4
  declare const GeneratorVersionSchema: Schema.Struct<{
5
5
  version: typeof Schema.String;
6
- commitHash: typeof Schema.String;
7
6
  generatedAt: typeof Schema.String;
8
7
  }>;
9
8
  export type GeneratorVersion = typeof GeneratorVersionSchema.Type;
10
9
  export declare const readVersion: (directory: string) => Effect.Effect<{
11
10
  readonly version: string;
12
- readonly commitHash: string;
13
11
  readonly generatedAt: string;
14
12
  }, GeneratorVersionError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
15
13
  export declare const writeVersion: (directory: string, version: GeneratorVersion) => Effect.Effect<void, import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
@@ -20,7 +18,6 @@ export declare const writeVersion: (directory: string, version: GeneratorVersion
20
18
  */
21
19
  export declare const assertValidProject: (directory: string) => Effect.Effect<{
22
20
  readonly version: string;
23
- readonly commitHash: string;
24
21
  readonly generatedAt: string;
25
22
  }, GeneratorVersionError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
26
23
  export {};
@@ -1,9 +1,8 @@
1
- import { Console, Effect, Schema } from 'effect';
2
1
  import { FileSystem } from '@effect/platform';
2
+ import { Console, Effect, Schema } from 'effect';
3
3
  import { GeneratorVersionError } from '../errors.js';
4
4
  const GeneratorVersionSchema = Schema.Struct({
5
5
  version: Schema.String,
6
- commitHash: Schema.String,
7
6
  generatedAt: Schema.String,
8
7
  });
9
8
  const VERSION_FILE = '.generator-version';
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from '@effect/cli';
3
+ import { NodeContext, NodeRuntime } from '@effect/platform-node';
4
+ import { Console, Effect, Layer } from 'effect';
5
+ import { readFileSync } from 'node:fs';
6
+ import { dirname, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { generateCommand } from './commands/generate.js';
9
+ import { initCommand } from './commands/init.js';
10
+ import { releasesCommand } from './commands/releases.js';
11
+ import { updateCommand } from './commands/update.js';
12
+ import { mcpCommand, ServerLayer } from './mcp.js';
13
+ import { GithubReleaseLayer } from './services/release.js';
14
+ function readCliVersion() {
15
+ try {
16
+ const raw = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf8');
17
+ const parsed = JSON.parse(raw);
18
+ if (!parsed.version)
19
+ throw new Error('Missing "version" field in package.json');
20
+ return parsed.version;
21
+ }
22
+ catch (err) {
23
+ process.stderr.write(`Failed to read CLI version: ${String(err)}\n`);
24
+ process.exit(1);
25
+ }
26
+ }
27
+ const version = readCliVersion();
28
+ 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([
29
+ initCommand,
30
+ generateCommand,
31
+ updateCommand,
32
+ releasesCommand,
33
+ mcpCommand,
34
+ ]));
35
+ const cli = Command.run(rootCommand, {
36
+ name: 'ping-lf',
37
+ version,
38
+ });
39
+ // Detect `ping-lf mcp` early so we can launch the MCP server via Layer.launch,
40
+ // which cannot be composed with the @effect/cli Effect pipeline. When --help or
41
+ // -h is present we fall through to the CLI so the user still gets usage output.
42
+ const isMcpLaunch = process.argv[2] === 'mcp' && !process.argv.includes('--help') && !process.argv.includes('-h');
43
+ const program = isMcpLaunch
44
+ ? Layer.launch(ServerLayer)
45
+ : cli(process.argv).pipe(Effect.catchTag('DirectoryConflictError', (err) => Console.error(`\nError: "${err.path}" already contains a framework project.` +
46
+ `\n Did you mean to use --local?\n` +
47
+ `\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.` +
48
+ `\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` +
49
+ ` ${err.cause}\n\n` +
50
+ ` • Check your network connection and try again.\n` +
51
+ ` • Use a local path: ping-lf init <dir> --local <path>\n` +
52
+ ` • Specify a tag: ping-lf init <dir> --tag @forgerock/login-widget@2.1.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` +
53
+ ` Expected a Git tag like @forgerock/login-widget@2.1.0 or v1.0.0.\n` +
54
+ ` 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.` +
55
+ (err.cause ? `\n ${err.cause}` : '') +
56
+ `\n\n • Check your network connection and try again.\n` +
57
+ ` • 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` +
58
+ ` Names must be PascalCase, start with an uppercase letter, and contain only letters and digits.\n` +
59
+ ` 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` +
60
+ ` Choose a different name or delete the existing directory first.\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.provide(Layer.mergeAll(GithubReleaseLayer, NodeContext.layer)));
61
+ NodeRuntime.runMain(program, { disableErrorReporting: true });
@@ -0,0 +1,52 @@
1
+ import { Tool, Toolkit } from '@effect/ai';
2
+ import { Command } from '@effect/cli';
3
+ import { Layer, Schema } from 'effect';
4
+ export declare const mcpToolkit: Toolkit.Toolkit<{
5
+ readonly init: Tool.Tool<"init", {
6
+ readonly parameters: Schema.Struct<{
7
+ directory: Schema.SchemaClass<string, string, never>;
8
+ version: Schema.optional<typeof Schema.String>;
9
+ local: Schema.optional<typeof Schema.String>;
10
+ }>;
11
+ readonly success: typeof Schema.String;
12
+ readonly failure: typeof Schema.String;
13
+ readonly failureMode: "error";
14
+ }, never>;
15
+ readonly generate_callback: Tool.Tool<"generate_callback", {
16
+ readonly parameters: Schema.Struct<{
17
+ name: Schema.SchemaClass<string, string, never>;
18
+ directory: Schema.optional<typeof Schema.String>;
19
+ }>;
20
+ readonly success: typeof Schema.String;
21
+ readonly failure: typeof Schema.String;
22
+ readonly failureMode: "error";
23
+ }, never>;
24
+ readonly generate_stage: Tool.Tool<"generate_stage", {
25
+ readonly parameters: Schema.Struct<{
26
+ name: Schema.SchemaClass<string, string, never>;
27
+ directory: Schema.optional<typeof Schema.String>;
28
+ }>;
29
+ readonly success: typeof Schema.String;
30
+ readonly failure: typeof Schema.String;
31
+ readonly failureMode: "error";
32
+ }, never>;
33
+ readonly update: Tool.Tool<"update", {
34
+ readonly parameters: Schema.Struct<{
35
+ directory: Schema.optional<typeof Schema.String>;
36
+ version: Schema.optional<typeof Schema.String>;
37
+ local: Schema.optional<typeof Schema.String>;
38
+ }>;
39
+ readonly success: typeof Schema.String;
40
+ readonly failure: typeof Schema.String;
41
+ readonly failureMode: "error";
42
+ }, never>;
43
+ readonly list_releases: Tool.Tool<"list_releases", {
44
+ readonly parameters: Schema.Struct<{}>;
45
+ readonly success: typeof Schema.String;
46
+ readonly failure: typeof Schema.String;
47
+ readonly failureMode: "error";
48
+ }, never>;
49
+ }>;
50
+ declare const ServerLayer: Layer.Layer<never, never, never>;
51
+ export { ServerLayer };
52
+ export declare const mcpCommand: Command.Command<"mcp", never, never, {}>;