@forgerock/login-framework-cli 0.0.0-beta.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -34
- package/dist/{commands → src/commands}/generate.d.ts +3 -2
- package/dist/{commands → src/commands}/generate.js +15 -8
- package/dist/src/commands/init.d.ts +16 -0
- package/dist/{commands → src/commands}/init.js +8 -10
- package/dist/{commands → src/commands}/source.d.ts +1 -1
- package/dist/{commands → src/commands}/update.d.ts +4 -2
- package/dist/{commands → src/commands}/update.js +7 -6
- package/dist/{config → src/config}/version.d.ts +1 -4
- package/dist/{config → src/config}/version.js +1 -2
- package/dist/src/main.js +61 -0
- package/dist/src/mcp.d.ts +52 -0
- package/dist/src/mcp.js +182 -0
- package/dist/{services → src/services}/registry.js +2 -2
- package/dist/{services → src/services}/release.d.ts +2 -1
- package/dist/src/templates/callback/__COMPONENT_SLUG__.mock.ts +22 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.stories.js +28 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.story.svelte +59 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.svelte +49 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +9 -0
- package/dist/{templates → src/templates}/callback/__COMPONENT_SLUG__.utilities.ts +1 -1
- package/dist/src/templates/stage/__COMPONENT_SLUG__.mock.ts +23 -0
- package/dist/src/templates/stage/__COMPONENT_SLUG__.stories.js +43 -0
- package/dist/src/templates/stage/__COMPONENT_SLUG__.story.svelte +47 -0
- package/dist/{templates → src/templates}/stage/__COMPONENT_SLUG__.svelte +21 -19
- package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +9 -0
- package/dist/{templates → src/templates}/stage/__COMPONENT_SLUG__.utilities.ts +1 -1
- package/dist/src/templates/tsconfig.json +18 -0
- package/dist/src/utils.d.ts +11 -0
- package/dist/src/utils.js +17 -0
- package/package.json +13 -10
- package/dist/commands/init.d.ts +0 -8
- package/dist/main.js +0 -34
- package/dist/templates/callback/__COMPONENT_SLUG__.svelte +0 -73
- package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +0 -9
- package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +0 -9
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -4
- package/scripts/generate-registry.mjs +0 -23
- package/dist/{commands → src/commands}/releases.d.ts +0 -0
- package/dist/{commands → src/commands}/releases.js +0 -0
- package/dist/{commands → src/commands}/source.js +0 -0
- package/dist/{config → src/config}/exclusions.d.ts +0 -0
- package/dist/{config → src/config}/exclusions.js +0 -0
- package/dist/{errors.d.ts → src/errors.d.ts} +8 -8
- package/dist/{errors.js → src/errors.js} +2 -2
- package/dist/{main.d.ts → src/main.d.ts} +0 -0
- package/dist/{services → src/services}/file-system.d.ts +1 -1
- package/dist/{services → src/services}/file-system.js +2 -2
- package/dist/{services → src/services}/registry.d.ts +1 -1
- package/dist/{services → src/services}/release.js +1 -1
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 @
|
|
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 @
|
|
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
|
|
39
|
-
|
|
|
40
|
-
| `--
|
|
41
|
-
| `--local <path>`
|
|
38
|
+
| Flag | Description |
|
|
39
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `--tag <tag>` | Download a specific release tag (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
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.
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
108
|
-
ping-lf update --version v1.5.0
|
|
109
|
-
ping-lf update --local ../framework
|
|
165
|
+
ping-lf update # update to latest
|
|
166
|
+
ping-lf update --version v1.5.0 # pin to a specific version
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
cd tools/cli
|
|
203
|
+
### Recommended setup (once per machine)
|
|
146
204
|
|
|
147
|
-
|
|
148
|
-
pnpm install
|
|
205
|
+
From the repo root, after `pnpm install`:
|
|
149
206
|
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
214
|
+
```sh
|
|
215
|
+
ping-lf init my-project
|
|
216
|
+
ping-lf generate callback MyCallback
|
|
217
|
+
ping-lf update --tag v1.5.0
|
|
158
218
|
```
|
|
159
219
|
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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 #
|
|
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
|
|
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 {
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
5
|
import nodePath from 'node:path';
|
|
6
|
-
import {
|
|
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
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
-
|
|
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 version: 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
|
|
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
|
-
})
|
|
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
|
+
version: Options.optional(Options.text('version').pipe(Options.withDescription('Framework release tag to download (e.g. v1.2.0). If omitted, the main branch is used.'))),
|
|
93
|
+
}, ({ directory, local, version }) => initProject({ directory, local, version })).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 |
|
|
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
|
|
4
|
-
readonly
|
|
4
|
+
readonly directory: Option.Option<string>;
|
|
5
|
+
readonly local: Option.Option<string>;
|
|
6
|
+
readonly version: 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
|
|
4
|
-
import {
|
|
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
11
|
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
|
+
}, ({ local, version, 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`);
|
|
@@ -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';
|
package/dist/src/main.js
ADDED
|
@@ -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 version: ping-lf init <dir> --version v1.0.0\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseParseError', (err) => Console.error(`\nError: Failed to parse the release data.\n ${err.cause}\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseFsError', (err) => Console.error(`\nError: Filesystem error during release download (${err.operation}).\n ${err.cause}\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('InvalidVersionError', (err) => Console.error(`\nError: "${err.version}" is not a valid version tag.\n` +
|
|
53
|
+
` Expected semver format like 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, {}>;
|