create-rakomi-app 0.1.1 → 0.2.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 +17 -0
- package/SECURITY.md +2 -2
- package/dist/agent-context.js +61 -0
- package/dist/client-registry.generated.js +157 -0
- package/dist/env.js +88 -11
- package/dist/index.js +33 -5
- package/dist/mcp-config.js +48 -0
- package/dist/prompt.js +29 -4
- package/dist/usage.js +22 -8
- package/package.json +2 -2
- package/sbom.cdx.json +3 -3
package/README.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
Scaffold a [Rakomi](https://rakomi.com) quickstart app in seconds. Rakomi is EU-native
|
|
4
4
|
authentication as a service.
|
|
5
5
|
|
|
6
|
+
## Ask your agent
|
|
7
|
+
|
|
8
|
+
Add the Rakomi MCP server to your coding agent, then ask it directly:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
claude mcp add --transport http rakomi https://mcp.rakomi.com/mcp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
"List the AI agents connected to my Rakomi tenant."
|
|
15
|
+
|
|
6
16
|
## Getting started
|
|
7
17
|
|
|
8
18
|
```sh
|
|
@@ -35,6 +45,8 @@ npm create @rakomi/rakomi-app@latest -- --template nextjs my-app
|
|
|
35
45
|
| `--tenant-id <value>` | your tenant id |
|
|
36
46
|
| `--template-source <url>` | override the archive base (mirror / offline) |
|
|
37
47
|
| `--yes` | accept defaults, never prompt (non-interactive) |
|
|
48
|
+
| `--connect` | print next steps for connecting an AI agent (the `rakomi` CLI) |
|
|
49
|
+
| `--no-mcp` | skip scaffolding `.mcp.json` / `AGENTS.md` (written by default) |
|
|
38
50
|
| `-h`, `--help` | show help |
|
|
39
51
|
| `-V`, `--version` | print the version |
|
|
40
52
|
|
|
@@ -43,6 +55,11 @@ local `.env` in the new project. Value precedence is: flag > environment variabl
|
|
|
43
55
|
default. In a non-interactive context (a pipe, a continuous-integration job, or `--yes`) it never
|
|
44
56
|
blocks — it uses flags / environment values / defaults and leaves the rest for you to fill in.
|
|
45
57
|
|
|
58
|
+
A project-scope `.mcp.json` and an `AGENTS.md` briefing are scaffolded alongside your new app by
|
|
59
|
+
default, so it is agent-ready on first run — open it in Claude Code and run `claude mcp login rakomi`
|
|
60
|
+
to finish sign-in, or point any other AI coding agent at `AGENTS.md` for the same information (the
|
|
61
|
+
MCP server URL, the connect step, and where credentials live). Pass `--no-mcp` to skip both files.
|
|
62
|
+
|
|
46
63
|
`RAKOMI_REGION` defaults to `eu-central` — a visible data-residency stance, not a mandate. Override
|
|
47
64
|
it with `--region` or the `RAKOMI_REGION` environment variable for any other region.
|
|
48
65
|
|
package/SECURITY.md
CHANGED
|
@@ -9,8 +9,8 @@ While these packages remain pre-1.0 (`0.x`), they carry **no stability or suppor
|
|
|
9
9
|
(SemVer 2.0.0 §4); the latest `0.x` line receives security updates on a best-effort basis.
|
|
10
10
|
|
|
11
11
|
From version **1.0** onward, Rakomi maintains the current (N) and previous (N-1) MAJOR in parallel, with N-1 receiving
|
|
12
|
-
security-only fixes.
|
|
13
|
-
**
|
|
12
|
+
security-only fixes. For each MAJOR version of the Rakomi SDKs, CRE8EVE commits to a CRA support period of
|
|
13
|
+
**at least five years (60 months)**, meeting the support-period requirement of **CRA Art. 13(8)**. The authoritative, machine-readable support windows are published at
|
|
14
14
|
[`https://api.rakomi.com/.well-known/sdk-support.json`](https://api.rakomi.com/.well-known/sdk-support.json)
|
|
15
15
|
and rendered for humans on the [SDK Support & Lifecycle page](https://rakomi.com/sdk-support). This
|
|
16
16
|
document points at that single source rather than re-typing dated rows.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { RAKOMI_SERVER_NAME } from './client-registry.generated.js';
|
|
5
|
+
import { ENV_KEYS } from './env.js';
|
|
6
|
+
import { CLAUDE_CODE_CLIENT, MCP_URL } from './mcp-config.js';
|
|
7
|
+
/** The agent-context filename this module writes, at the project root. */
|
|
8
|
+
export const AGENT_CONTEXT_FILENAME = 'AGENTS.md';
|
|
9
|
+
/**
|
|
10
|
+
* `{{MCP_URL}}` and `{{SERVER_NAME}}` are the registry's own placeholder tokens (`mcp-clients.json`)
|
|
11
|
+
* — substituted here, never baked into the generated module, so one registry entry serves every
|
|
12
|
+
* environment's MCP URL. The scaffolder always writes the default server name, so that is what the
|
|
13
|
+
* rendered instruction names. Mirrors `packages/cli/src/commands/connect.ts`'s helper and contract.
|
|
14
|
+
*/
|
|
15
|
+
function withMcpUrl(instruction, mcpUrl) {
|
|
16
|
+
return instruction.replaceAll('{{MCP_URL}}', mcpUrl).replaceAll('{{SERVER_NAME}}', RAKOMI_SERVER_NAME);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Render `AGENTS.md`'s full body for a freshly scaffolded project. Pure and deterministic (no
|
|
20
|
+
* timestamps, no random ids, no directory/template name) so the golden-content test can assert
|
|
21
|
+
* byte-for-byte output regardless of which template or target directory the scaffold used.
|
|
22
|
+
*/
|
|
23
|
+
export function agentContextMarkdown() {
|
|
24
|
+
const envKeyList = ENV_KEYS.map((k) => `\`${k}\``).join(', ');
|
|
25
|
+
return ([
|
|
26
|
+
'# Rakomi integration',
|
|
27
|
+
'',
|
|
28
|
+
'This project is wired to a Rakomi MCP server, so an AI coding agent working in this repository',
|
|
29
|
+
'can read and manage this tenant on your behalf, once you connect it.',
|
|
30
|
+
'',
|
|
31
|
+
'## MCP server',
|
|
32
|
+
'',
|
|
33
|
+
`- URL: \`${MCP_URL}\``,
|
|
34
|
+
'- Config: `.mcp.json` (already scaffolded in this project root)',
|
|
35
|
+
'',
|
|
36
|
+
'## Connect',
|
|
37
|
+
'',
|
|
38
|
+
withMcpUrl(CLAUDE_CODE_CLIENT.finishInstruction, MCP_URL),
|
|
39
|
+
'',
|
|
40
|
+
'## Credentials',
|
|
41
|
+
'',
|
|
42
|
+
`Tenant credentials live in \`.env\` (${envKeyList}) — never in this file, never in chat, and`,
|
|
43
|
+
'never committed (`.env` is not tracked by version control). An agent reading this project',
|
|
44
|
+
'should read values from `.env`, not ask you to paste them.',
|
|
45
|
+
'',
|
|
46
|
+
'## Ask your agent',
|
|
47
|
+
'',
|
|
48
|
+
'> "Connect to my Rakomi tenant using .mcp.json and show me my current users."',
|
|
49
|
+
'',
|
|
50
|
+
].join('\n') + '\n');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Write `AGENTS.md` into the freshly scaffolded project root. Always writes unconditionally when
|
|
54
|
+
* called (gated by the same `--no-mcp` opt-out as `.mcp.json` at the call site, `index.ts`) — a
|
|
55
|
+
* fresh quickstart template never ships its own `AGENTS.md` (verified by
|
|
56
|
+
* `test/agent-context.test.ts`'s manifest scan), so there is nothing to merge and nothing to
|
|
57
|
+
* clobber. LF-terminated, the same convention every other writer in this package uses.
|
|
58
|
+
*/
|
|
59
|
+
export async function writeAgentContext(targetDir) {
|
|
60
|
+
await writeFile(join(targetDir, AGENT_CONTEXT_FILENAME), agentContextMarkdown(), 'utf8');
|
|
61
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export const RAKOMI_SERVER_NAME = "rakomi";
|
|
3
|
+
export const CLIENT_REGISTRY = [
|
|
4
|
+
{
|
|
5
|
+
id: "claude-code",
|
|
6
|
+
displayName: "Claude Code",
|
|
7
|
+
tier: "written",
|
|
8
|
+
status: "verified",
|
|
9
|
+
finishInstruction: "1. Run `claude` in this directory and approve the \"{{SERVER_NAME}}\" project server when prompted.\n2. Then run `claude mcp login {{SERVER_NAME}}` to finish sign-in in your browser (or use `/mcp` inside that session).",
|
|
10
|
+
config: {
|
|
11
|
+
scope: "project",
|
|
12
|
+
pathTemplate: ".mcp.json",
|
|
13
|
+
serialization: "json",
|
|
14
|
+
topLevelKey: "mcpServers",
|
|
15
|
+
remoteUrlField: "url",
|
|
16
|
+
extraFields: { "type": "http" },
|
|
17
|
+
},
|
|
18
|
+
evidenceUrl: "https://code.claude.com/docs/en/mcp",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "vscode",
|
|
22
|
+
displayName: "VS Code",
|
|
23
|
+
tier: "written",
|
|
24
|
+
status: "documented-shape-only",
|
|
25
|
+
finishInstruction: "Open the Command Palette and run \"MCP: List Servers\", or the Extensions view — VS Code starts the OAuth flow automatically the first time it connects.",
|
|
26
|
+
config: {
|
|
27
|
+
scope: "project",
|
|
28
|
+
pathTemplate: ".vscode/mcp.json",
|
|
29
|
+
serialization: "json",
|
|
30
|
+
topLevelKey: "servers",
|
|
31
|
+
remoteUrlField: "url",
|
|
32
|
+
extraFields: { "type": "http" },
|
|
33
|
+
},
|
|
34
|
+
evidenceUrl: "https://code.visualstudio.com/docs/agents/reference/mcp-configuration",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "cursor",
|
|
38
|
+
displayName: "Cursor",
|
|
39
|
+
tier: "written",
|
|
40
|
+
status: "documented-shape-only",
|
|
41
|
+
finishInstruction: "Open Cursor's MCP settings and connect — for a server that supports OAuth, Cursor completes the flow for you (or accepts static client credentials in mcp.json instead of dynamic client registration).",
|
|
42
|
+
config: {
|
|
43
|
+
scope: "project",
|
|
44
|
+
pathTemplate: ".cursor/mcp.json",
|
|
45
|
+
serialization: "json",
|
|
46
|
+
topLevelKey: "mcpServers",
|
|
47
|
+
remoteUrlField: "url",
|
|
48
|
+
extraFields: {},
|
|
49
|
+
},
|
|
50
|
+
evidenceUrl: "https://cursor.com/docs/mcp",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "antigravity",
|
|
54
|
+
displayName: "Google Antigravity",
|
|
55
|
+
tier: "written",
|
|
56
|
+
status: "documented-shape-only",
|
|
57
|
+
finishInstruction: "Open the MCP Store or your MCP settings in Antigravity — it handles OAuth automatically for servers that support dynamic client registration (DCR).",
|
|
58
|
+
config: {
|
|
59
|
+
scope: "project",
|
|
60
|
+
pathTemplate: ".agents/mcp_config.json",
|
|
61
|
+
serialization: "json",
|
|
62
|
+
topLevelKey: "mcpServers",
|
|
63
|
+
remoteUrlField: "serverUrl",
|
|
64
|
+
extraFields: {},
|
|
65
|
+
},
|
|
66
|
+
evidenceUrl: "https://antigravity.google/docs/ide/mcp/",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "gemini-cli",
|
|
70
|
+
displayName: "Gemini CLI",
|
|
71
|
+
tier: "written",
|
|
72
|
+
status: "documented-shape-only",
|
|
73
|
+
finishInstruction: "Run `gemini mcp list`, or just start a session — the Gemini CLI supports OAuth 2.0 for remote MCP servers and negotiates it automatically.",
|
|
74
|
+
config: {
|
|
75
|
+
scope: "project",
|
|
76
|
+
pathTemplate: ".gemini/settings.json",
|
|
77
|
+
serialization: "json",
|
|
78
|
+
topLevelKey: "mcpServers",
|
|
79
|
+
remoteUrlField: "httpUrl",
|
|
80
|
+
extraFields: {},
|
|
81
|
+
},
|
|
82
|
+
evidenceUrl: "https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "zed",
|
|
86
|
+
displayName: "Zed",
|
|
87
|
+
tier: "written",
|
|
88
|
+
status: "documented-shape-only",
|
|
89
|
+
finishInstruction: "Open Settings -> AI -> MCP Servers in Zed — when a remote server has no Authorization header configured, Zed prompts you to authenticate with the standard MCP OAuth flow.",
|
|
90
|
+
config: {
|
|
91
|
+
scope: "project",
|
|
92
|
+
pathTemplate: ".zed/settings.json",
|
|
93
|
+
serialization: "json",
|
|
94
|
+
topLevelKey: "context_servers",
|
|
95
|
+
remoteUrlField: "url",
|
|
96
|
+
extraFields: {},
|
|
97
|
+
},
|
|
98
|
+
evidenceUrl: "https://zed.dev/docs/ai/mcp",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "codex-cli",
|
|
102
|
+
displayName: "OpenAI Codex CLI",
|
|
103
|
+
tier: "written",
|
|
104
|
+
status: "documented-shape-only",
|
|
105
|
+
finishInstruction: "Run `codex mcp login {{SERVER_NAME}}` to finish sign-in in your browser.",
|
|
106
|
+
config: {
|
|
107
|
+
scope: "user",
|
|
108
|
+
pathTemplate: ".codex/config.toml",
|
|
109
|
+
serialization: "toml",
|
|
110
|
+
topLevelKey: "mcp_servers",
|
|
111
|
+
remoteUrlField: "url",
|
|
112
|
+
extraFields: {},
|
|
113
|
+
},
|
|
114
|
+
evidenceUrl: "https://learn.chatgpt.com/docs/extend/mcp?surface=cli",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "devin-desktop",
|
|
118
|
+
displayName: "Devin Desktop",
|
|
119
|
+
tier: "written",
|
|
120
|
+
status: "documented-shape-only",
|
|
121
|
+
finishInstruction: "Open Devin Desktop — it supports OAuth for each transport type and prompts you to connect the first time.",
|
|
122
|
+
config: {
|
|
123
|
+
scope: "user",
|
|
124
|
+
pathTemplate: ".codeium/windsurf/mcp_config.json",
|
|
125
|
+
serialization: "json",
|
|
126
|
+
topLevelKey: "mcpServers",
|
|
127
|
+
remoteUrlField: "serverUrl",
|
|
128
|
+
extraFields: {},
|
|
129
|
+
},
|
|
130
|
+
evidenceUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "claude-desktop",
|
|
134
|
+
displayName: "Claude Desktop",
|
|
135
|
+
tier: "instructed",
|
|
136
|
+
status: "verified",
|
|
137
|
+
finishInstruction: "Claude Desktop connects to remote MCP servers through Connectors, configured from your Claude account rather than a local file — there is nothing for `rakomi connect` to write.\n\n1. Open Settings -> Connectors in Claude Desktop and add a custom connector.\n2. Enter Rakomi's MCP server URL: {{MCP_URL}}\n3. Click Connect. Claude Desktop opens your browser at accounts.rakomi.com — sign in and\n approve the read-only consent screen.",
|
|
138
|
+
evidenceUrl: "https://support.claude.com/en/articles/11175166-about-custom-connectors-remote-mcp",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "chatgpt",
|
|
142
|
+
displayName: "ChatGPT",
|
|
143
|
+
tier: "instructed",
|
|
144
|
+
status: "documented-shape-only",
|
|
145
|
+
finishInstruction: "In ChatGPT, go to Workspace Settings -> Permissions & Roles -> Developer mode, then add {{MCP_URL}} as a remote server. ChatGPT starts an OAuth flow to your workspace's identity provider once it's added.",
|
|
146
|
+
evidenceUrl: "https://developers.openai.com/api/docs/mcp",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "jetbrains",
|
|
150
|
+
displayName: "JetBrains AI Assistant",
|
|
151
|
+
tier: "instructed",
|
|
152
|
+
status: "documented-shape-only",
|
|
153
|
+
finishInstruction: "In your JetBrains IDE, go to Settings -> Tools -> AI Assistant -> Model Context Protocol (MCP), click Add, and paste: {\"mcpServers\":{\"{{SERVER_NAME}}\":{\"url\":\"{{MCP_URL}}\"}}}\nJetBrains' own documentation does not state whether it drives OAuth for you — be ready to complete sign-in in whatever browser tab it opens.",
|
|
154
|
+
evidenceUrl: "https://www.jetbrains.com/help/ai-assistant/mcp.html",
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
export const KNOWN_CLIENTS = CLIENT_REGISTRY.map((c) => c.id);
|
package/dist/env.js
CHANGED
|
@@ -1,12 +1,86 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import { writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Keys the scaffolder collects, in written order. Names follow `RAKOMI_[A-Z0-9_]+`.
|
|
6
|
+
* `RAKOMI_CLIENT_SECRET`/`RAKOMI_REDIRECT_URI` are collected ONLY for the `node` template — see
|
|
7
|
+
* `NODE_ONLY_KEYS` below; every other template's dev server never reads either.
|
|
8
|
+
*/
|
|
9
|
+
export const ENV_KEYS = [
|
|
10
|
+
'RAKOMI_REGION',
|
|
11
|
+
'RAKOMI_TENANT_ID',
|
|
12
|
+
'RAKOMI_API_KEY',
|
|
13
|
+
'RAKOMI_CLIENT_ID',
|
|
14
|
+
'RAKOMI_CLIENT_SECRET',
|
|
15
|
+
'RAKOMI_REDIRECT_URI',
|
|
16
|
+
'RAKOMI_ISSUER',
|
|
17
|
+
];
|
|
6
18
|
/** The default EU region — a visible, overridable data-residency stance, not a mandate. */
|
|
7
19
|
export const DEFAULT_REGION = 'eu-central';
|
|
8
|
-
/**
|
|
9
|
-
|
|
20
|
+
/** The node quickstart's own `.env.example` default — an OAuth client registered for local dev
|
|
21
|
+
* typically has this exact redirect URI, so it doubles as a working starting point. */
|
|
22
|
+
export const DEFAULT_NODE_REDIRECT_URI = 'http://localhost:3000/callback';
|
|
23
|
+
/**
|
|
24
|
+
* Keys whose value is a credential and must never be echoed to stdout / logs / summaries.
|
|
25
|
+
* `RAKOMI_CLIENT_ID` is deliberately absent — an OAuth client_id is a PUBLIC, publishable
|
|
26
|
+
* identifier (PKCE public clients ship it in bundled JS), not a secret.
|
|
27
|
+
*/
|
|
28
|
+
export const SECRET_KEYS = new Set(['RAKOMI_API_KEY', 'RAKOMI_CLIENT_SECRET']);
|
|
29
|
+
/**
|
|
30
|
+
* The `node` quickstart is the only template whose OWN server performs a confidential-client
|
|
31
|
+
* OAuth code exchange (`examples/quickstarts/node/src/config.ts`) — `RAKOMI_REDIRECT_URI` is
|
|
32
|
+
* REQUIRED there (`loadConfig()` throws a `ConfigError` at boot without it: the scaffolder used
|
|
33
|
+
* to omit it entirely, so a freshly-scaffolded node app never started) and `RAKOMI_CLIENT_SECRET`
|
|
34
|
+
* is read for the confidential-client case node's own README instructs the user to set up
|
|
35
|
+
* (optional — an unset value degrades to a public/PKCE-only client, per that file's own comment).
|
|
36
|
+
* Every browser-based template (nextjs/react/expo) drives its OAuth flow through `@rakomi/react`
|
|
37
|
+
* client-side and never reads either var — collecting/writing them there would be a meaningless
|
|
38
|
+
* prompt and a dead `.env` line for those templates.
|
|
39
|
+
*/
|
|
40
|
+
const NODE_ONLY_KEYS = new Set(['RAKOMI_CLIENT_SECRET', 'RAKOMI_REDIRECT_URI']);
|
|
41
|
+
/**
|
|
42
|
+
* `RAKOMI_ISSUER` — the issuer of the environment whose tokens the app accepts (every environment
|
|
43
|
+
* is its own issuer with its own signing keys). Read, and required at boot / per request, ONLY by
|
|
44
|
+
* the templates whose own server verifies access tokens: `node` (`src/config.ts`) and `nextjs`
|
|
45
|
+
* (`src/lib/server/config.ts`). The browser/mobile templates never verify a token themselves.
|
|
46
|
+
*/
|
|
47
|
+
const ISSUER_TEMPLATES = new Set(['node', 'nextjs']);
|
|
48
|
+
/** The keys actually relevant to a given template — `ENV_KEYS` minus the `node`-only keys for
|
|
49
|
+
* every other template (or an absent/unknown slug — the conservative default), unchanged (full
|
|
50
|
+
* set) for `node` itself. Drives both what the wizard prompts for (`prompt.ts`'s `collectEnv`)
|
|
51
|
+
* and what `renderDotenv` writes. */
|
|
52
|
+
export function keysForTemplate(templateSlug) {
|
|
53
|
+
if (templateSlug === 'node')
|
|
54
|
+
return ENV_KEYS;
|
|
55
|
+
return ENV_KEYS.filter((key) => !NODE_ONLY_KEYS.has(key) &&
|
|
56
|
+
(key !== 'RAKOMI_ISSUER' || (templateSlug !== undefined && ISSUER_TEMPLATES.has(templateSlug))));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every scaffolded template reads `RAKOMI_REGION` / `RAKOMI_TENANT_ID` / `RAKOMI_API_KEY` under
|
|
60
|
+
* their canonical `RAKOMI_*` name — but each template's OWN `.env.example` names its OAuth
|
|
61
|
+
* `client_id` differently, following that framework's inline-at-build-time convention:
|
|
62
|
+
* Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`, Expo `EXPO_PUBLIC_*`; a server-only Node app keeps the
|
|
63
|
+
* plain `RAKOMI_*` form. Writing the collected value under the WRONG name means the app never
|
|
64
|
+
* reads it and fails with a first-run "missing client" error despite a fully-filled `.env`
|
|
65
|
+
* (the incident this mapping exists to close). See `examples/quickstarts/{slug}/.env.example`
|
|
66
|
+
* for the source of truth this table mirrors.
|
|
67
|
+
*/
|
|
68
|
+
const CLIENT_ID_KEY_BY_TEMPLATE = {
|
|
69
|
+
nextjs: 'NEXT_PUBLIC_RAKOMI_CLIENT_ID',
|
|
70
|
+
react: 'VITE_RAKOMI_CLIENT_ID',
|
|
71
|
+
expo: 'EXPO_PUBLIC_RAKOMI_CLIENT_ID',
|
|
72
|
+
node: 'RAKOMI_CLIENT_ID',
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the `.env` key NAME a given canonical `EnvKey` must be written under for a specific
|
|
76
|
+
* template. Only `RAKOMI_CLIENT_ID` varies by template today; every other key keeps its
|
|
77
|
+
* canonical `RAKOMI_*` name across every template.
|
|
78
|
+
*/
|
|
79
|
+
export function envKeyNameForTemplate(templateSlug, key) {
|
|
80
|
+
if (key !== 'RAKOMI_CLIENT_ID')
|
|
81
|
+
return key;
|
|
82
|
+
return CLIENT_ID_KEY_BY_TEMPLATE[templateSlug] ?? key;
|
|
83
|
+
}
|
|
10
84
|
const CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
|
|
11
85
|
/**
|
|
12
86
|
* Serialise a single `KEY=value` line in canonical dotenv form:
|
|
@@ -23,15 +97,18 @@ export function dotenvLine(key, rawValue) {
|
|
|
23
97
|
return `${key}="${escaped}"`;
|
|
24
98
|
}
|
|
25
99
|
/**
|
|
26
|
-
* Render a full `.env` body from collected values. Always LF-terminated, one key per line,
|
|
27
|
-
*
|
|
28
|
-
*
|
|
100
|
+
* Render a full `.env` body from collected values. Always LF-terminated, one key per line, in
|
|
101
|
+
* `ENV_KEYS` order restricted to `keysForTemplate(templateSlug)` — a key the given template never
|
|
102
|
+
* reads is never written at all, rather than left as a dead empty assignment. A missing value for
|
|
103
|
+
* a key the template DOES read is still written as an empty assignment so the file lists it for
|
|
104
|
+
* the user to complete. Each key is written under the NAME the given template actually reads
|
|
105
|
+
* (`envKeyNameForTemplate`) — not necessarily its canonical `RAKOMI_*` form.
|
|
29
106
|
*/
|
|
30
|
-
export function renderDotenv(values) {
|
|
31
|
-
const lines =
|
|
107
|
+
export function renderDotenv(values, templateSlug) {
|
|
108
|
+
const lines = keysForTemplate(templateSlug).map((key) => dotenvLine(envKeyNameForTemplate(templateSlug, key), values[key] ?? ''));
|
|
32
109
|
return lines.join('\n') + '\n';
|
|
33
110
|
}
|
|
34
111
|
/** Write the `.env` file into the target project directory. */
|
|
35
|
-
export async function writeEnvFile(targetDir, values) {
|
|
36
|
-
await writeFile(join(targetDir, '.env'), renderDotenv(values), 'utf8');
|
|
112
|
+
export async function writeEnvFile(targetDir, values, templateSlug) {
|
|
113
|
+
await writeFile(join(targetDir, '.env'), renderDotenv(values, templateSlug), 'utf8');
|
|
37
114
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { appendFile, readFile } from 'node:fs/promises';
|
|
5
5
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
|
+
import { writeAgentContext } from './agent-context.js';
|
|
9
10
|
import { writeEnvFile } from './env.js';
|
|
10
11
|
import { CliError, EXIT, UsageError } from './errors.js';
|
|
12
|
+
import { writeMcpConfig } from './mcp-config.js';
|
|
11
13
|
import { collectEnv, createTtyAsk } from './prompt.js';
|
|
12
14
|
import { assertTargetWritable, GithubCodeloadSource, materializeArchive } from './source.js';
|
|
13
15
|
import { findTemplate, slugList } from './templates.js';
|
|
@@ -45,9 +47,11 @@ async function dispatch(args, deps) {
|
|
|
45
47
|
template: { type: 'string' },
|
|
46
48
|
region: { type: 'string' },
|
|
47
49
|
'tenant-id': { type: 'string' },
|
|
50
|
+
'client-id': { type: 'string' },
|
|
48
51
|
'template-source': { type: 'string' },
|
|
49
52
|
yes: { type: 'boolean' },
|
|
50
53
|
connect: { type: 'boolean' },
|
|
54
|
+
'no-mcp': { type: 'boolean' },
|
|
51
55
|
help: { type: 'boolean', short: 'h' },
|
|
52
56
|
version: { type: 'boolean', short: 'V' },
|
|
53
57
|
},
|
|
@@ -82,15 +86,22 @@ async function dispatch(args, deps) {
|
|
|
82
86
|
flags.RAKOMI_REGION = values.region;
|
|
83
87
|
if (typeof values['tenant-id'] === 'string')
|
|
84
88
|
flags.RAKOMI_TENANT_ID = values['tenant-id'];
|
|
89
|
+
if (typeof values['client-id'] === 'string')
|
|
90
|
+
flags.RAKOMI_CLIENT_ID = values['client-id'];
|
|
85
91
|
const interactive = deps.isTTY && values.yes !== true && !deps.env.CI;
|
|
86
|
-
const envValues = await collectEnv({ flags, env: deps.env, interactive, ask: deps.ask });
|
|
92
|
+
const envValues = await collectEnv({ flags, env: deps.env, interactive, ask: deps.ask }, template.slug);
|
|
87
93
|
const source = deps.source ?? makeDefaultSource(values, deps.env);
|
|
88
94
|
const archive = await source.fetchArchive(template);
|
|
89
95
|
await materializeArchive(archive, targetDir);
|
|
90
96
|
await ensureEnvIgnored(targetDir);
|
|
91
|
-
await writeEnvFile(targetDir, envValues);
|
|
97
|
+
await writeEnvFile(targetDir, envValues, template.slug);
|
|
98
|
+
const mcpConfigWritten = values['no-mcp'] !== true;
|
|
99
|
+
if (mcpConfigWritten) {
|
|
100
|
+
await writeMcpConfig(targetDir);
|
|
101
|
+
await writeAgentContext(targetDir);
|
|
102
|
+
}
|
|
92
103
|
const pm = detectPackageManager(deps.env.npm_config_user_agent);
|
|
93
|
-
deps.stdout.write(postInstallMessage(template.slug, rawTarget, pm));
|
|
104
|
+
deps.stdout.write(postInstallMessage(template.slug, rawTarget, pm, mcpConfigWritten));
|
|
94
105
|
if (values.connect === true) {
|
|
95
106
|
deps.stdout.write(connectInstructions());
|
|
96
107
|
}
|
|
@@ -160,6 +171,23 @@ async function main() {
|
|
|
160
171
|
ask: createTtyAsk(),
|
|
161
172
|
});
|
|
162
173
|
}
|
|
163
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Whether this module is the process entry point. npm/npx invoke the bin through a symlink in
|
|
176
|
+
* `node_modules/.bin`, and `pathToFileURL` does not resolve symlinks — comparing against the raw
|
|
177
|
+
* `argv[1]` is false under npx, so the CLI exits 0 having done nothing. Resolve the real path
|
|
178
|
+
* first; fall back to the raw path when it cannot be resolved.
|
|
179
|
+
*/
|
|
180
|
+
function isProcessEntry(argv1) {
|
|
181
|
+
if (!argv1)
|
|
182
|
+
return false;
|
|
183
|
+
let real = argv1;
|
|
184
|
+
try {
|
|
185
|
+
real = realpathSync(argv1);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
}
|
|
189
|
+
return import.meta.url === pathToFileURL(real).href || import.meta.url === pathToFileURL(argv1).href;
|
|
190
|
+
}
|
|
191
|
+
if (isProcessEntry(process.argv[1])) {
|
|
164
192
|
void main();
|
|
165
193
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CLIENT_REGISTRY } from './client-registry.generated.js';
|
|
5
|
+
import { CliError, EXIT } from './errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* The Rakomi MCP resource server's canonical URL. Byte-frozen: `https` scheme, lowercase host,
|
|
8
|
+
* no port, the `/mcp` Streamable-HTTP mount path. Never hand-type this literal a second time —
|
|
9
|
+
* import this constant.
|
|
10
|
+
*/
|
|
11
|
+
export const MCP_URL = 'https://mcp.rakomi.com/mcp';
|
|
12
|
+
function mustFindClient(id) {
|
|
13
|
+
const entry = CLIENT_REGISTRY.find((c) => c.id === id);
|
|
14
|
+
if (!entry) {
|
|
15
|
+
throw new CliError(`client-registry.generated.ts is missing the "${id}" entry — run \`node scripts/gen-mcp-clients.mjs\` and commit the result.`, EXIT.FAIL);
|
|
16
|
+
}
|
|
17
|
+
return entry;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Claude Code's registry entry — the single source `mcpConfigJson()` and `agent-context.ts` derive
|
|
21
|
+
* the `.mcp.json` shape and the connect `finishInstruction` from. Never hand-duplicate either;
|
|
22
|
+
* import from here.
|
|
23
|
+
*/
|
|
24
|
+
export const CLAUDE_CODE_CLIENT = mustFindClient('claude-code');
|
|
25
|
+
/**
|
|
26
|
+
* The exact content written to `.mcp.json` — matches the shape `claude mcp add --transport http`
|
|
27
|
+
* produces (topLevelKey `mcpServers`, remoteUrlField `url`, extraFields `{ type: "http" }`),
|
|
28
|
+
* DERIVED from `CLAUDE_CODE_CLIENT.config` rather than a second hand-typed literal. No
|
|
29
|
+
* `client_id` — CIMD discovery needs none.
|
|
30
|
+
*/
|
|
31
|
+
export function mcpConfigJson() {
|
|
32
|
+
const config = CLAUDE_CODE_CLIENT.config;
|
|
33
|
+
if (!config) {
|
|
34
|
+
throw new CliError('The claude-code registry entry carries no config block.', EXIT.FAIL);
|
|
35
|
+
}
|
|
36
|
+
const rakomiEntry = { ...config.extraFields, [config.remoteUrlField]: MCP_URL };
|
|
37
|
+
return { [config.topLevelKey]: { rakomi: rakomiEntry } };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Write `.mcp.json` into the freshly scaffolded project root. Always writes unconditionally — a
|
|
41
|
+
* fresh quickstart template never ships its own `.mcp.json` (verified by
|
|
42
|
+
* `test/mcp-config.test.ts`'s manifest scan), so there is nothing to merge and nothing to clobber.
|
|
43
|
+
* Pretty-printed, LF-terminated, the same convention every other writer in this package uses
|
|
44
|
+
* (`env.ts`'s `writeEnvFile`).
|
|
45
|
+
*/
|
|
46
|
+
export async function writeMcpConfig(targetDir) {
|
|
47
|
+
await writeFile(join(targetDir, '.mcp.json'), JSON.stringify(mcpConfigJson(), null, 2) + '\n', 'utf8');
|
|
48
|
+
}
|
package/dist/prompt.js
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import { stdin, stdout } from 'node:process';
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
|
-
import { DEFAULT_REGION, ENV_KEYS, SECRET_KEYS } from './env.js';
|
|
4
|
+
import { DEFAULT_NODE_REDIRECT_URI, DEFAULT_REGION, ENV_KEYS, keysForTemplate, SECRET_KEYS } from './env.js';
|
|
5
5
|
export const FIELDS = [
|
|
6
6
|
{ key: 'RAKOMI_REGION', label: 'Data region', defaultValue: DEFAULT_REGION },
|
|
7
7
|
{ key: 'RAKOMI_TENANT_ID', label: 'Tenant ID' },
|
|
8
8
|
{ key: 'RAKOMI_API_KEY', label: 'API key' },
|
|
9
|
+
{ key: 'RAKOMI_CLIENT_ID', label: 'OAuth Client ID' },
|
|
10
|
+
{ key: 'RAKOMI_CLIENT_SECRET', label: 'OAuth Client Secret (leave blank for a public/PKCE-only client)' },
|
|
11
|
+
{ key: 'RAKOMI_REDIRECT_URI', label: 'Redirect URI', defaultValue: DEFAULT_NODE_REDIRECT_URI },
|
|
12
|
+
{ key: 'RAKOMI_ISSUER', label: 'Issuer of your environment' },
|
|
9
13
|
];
|
|
14
|
+
/**
|
|
15
|
+
* Extra guidance appended to a field's prompt line — where to GET a value the wizard cannot
|
|
16
|
+
* derive on its own, or a precondition the value itself carries. `RAKOMI_CLIENT_ID`: every tenant
|
|
17
|
+
* signup auto-provisions a default OAuth client at creation time, so the value already exists —
|
|
18
|
+
* find it rather than create it. `RAKOMI_API_KEY`: a signup's LIVE key (`akm_live_`) does not
|
|
19
|
+
* authenticate requests until the account owner verifies their e-mail — paste the TEST key
|
|
20
|
+
* (`akm_test_`) here to get this app running immediately, then swap in the live key once verified.
|
|
21
|
+
*/
|
|
22
|
+
const FIELD_HINTS = {
|
|
23
|
+
RAKOMI_API_KEY: 'paste the test key (akm_test_...) to start now — the live key needs email verification first',
|
|
24
|
+
RAKOMI_CLIENT_ID: 'from your Rakomi dashboard -> Settings -> Development credentials',
|
|
25
|
+
RAKOMI_ISSUER: 'the iss of your environment\'s tokens, e.g. https://api.rakomi.com/t/tn_.../test',
|
|
26
|
+
};
|
|
10
27
|
/**
|
|
11
28
|
* Resolve all env values by precedence: explicit flag > `RAKOMI_*` env var > interactive
|
|
12
29
|
* prompt > documented default. Never prompts in non-interactive mode (so a CI pipe never
|
|
13
30
|
* hangs); there it falls back to env/flag/default, leaving the rest empty for the user.
|
|
14
|
-
* Secret values are never echoed back.
|
|
31
|
+
* Secret values are never echoed back. `templateSlug` restricts which fields are even asked
|
|
32
|
+
* about — a key the given template never reads (see `keysForTemplate`) is skipped entirely,
|
|
33
|
+
* never prompted for and never present in the returned object; optional so every pre-existing
|
|
34
|
+
* caller/fixture (which meant "every canonical key applies") keeps compiling and behaving
|
|
35
|
+
* unchanged.
|
|
15
36
|
*/
|
|
16
|
-
export async function collectEnv(deps) {
|
|
37
|
+
export async function collectEnv(deps, templateSlug) {
|
|
38
|
+
const relevant = new Set(keysForTemplate(templateSlug));
|
|
17
39
|
const out = {};
|
|
18
40
|
for (const field of FIELDS) {
|
|
41
|
+
if (!relevant.has(field.key))
|
|
42
|
+
continue;
|
|
19
43
|
const fromFlag = deps.flags[field.key];
|
|
20
44
|
if (fromFlag !== undefined && fromFlag !== '') {
|
|
21
45
|
out[field.key] = fromFlag;
|
|
@@ -38,8 +62,9 @@ export async function collectEnv(deps) {
|
|
|
38
62
|
}
|
|
39
63
|
function promptText(field) {
|
|
40
64
|
const secret = SECRET_KEYS.has(field.key) ? ' (kept local, never sent anywhere)' : '';
|
|
65
|
+
const hint = FIELD_HINTS[field.key] ? ` (${FIELD_HINTS[field.key]})` : '';
|
|
41
66
|
const dflt = field.defaultValue !== undefined ? ` [${field.defaultValue}]` : '';
|
|
42
|
-
return `${field.label}${secret}${dflt}: `;
|
|
67
|
+
return `${field.label}${secret}${hint}${dflt}: `;
|
|
43
68
|
}
|
|
44
69
|
/** A real-TTY prompt backed by `node:readline/promises`. */
|
|
45
70
|
export function createTtyAsk() {
|
package/dist/usage.js
CHANGED
|
@@ -36,16 +36,22 @@ export function helpText() {
|
|
|
36
36
|
' --template <slug> which quickstart to scaffold (required)',
|
|
37
37
|
' --region <value> data region (default: eu-central)',
|
|
38
38
|
' --tenant-id <value> your tenant id',
|
|
39
|
+
' --client-id <value> your OAuth client_id (dashboard -> Integration -> Applications)',
|
|
39
40
|
' --template-source <url> override the archive base (mirror / offline)',
|
|
40
41
|
' --yes accept defaults, never prompt (non-interactive)',
|
|
41
42
|
' --connect print next steps for connecting an AI agent (the rakomi CLI)',
|
|
43
|
+
" --no-mcp don't scaffold .mcp.json / AGENTS.md (written by default — agent-ready on first run)",
|
|
42
44
|
' -h, --help show this help and exit',
|
|
43
45
|
' -V, --version print the version and exit',
|
|
44
46
|
'',
|
|
45
47
|
'Environment variables collected into the new project\'s .env:',
|
|
46
|
-
' RAKOMI_REGION, RAKOMI_TENANT_ID, RAKOMI_API_KEY',
|
|
48
|
+
' RAKOMI_REGION, RAKOMI_TENANT_ID, RAKOMI_API_KEY, RAKOMI_CLIENT_ID',
|
|
49
|
+
' RAKOMI_ISSUER (node, nextjs: the issuer of your environment, e.g. https://api.rakomi.com/t/tn_...)',
|
|
47
50
|
' (RAKOMI_API_KEY is read from the prompt or the environment, never a flag,',
|
|
48
|
-
' and is written only to your local .env — never transmitted.
|
|
51
|
+
' and is written only to your local .env — never transmitted. RAKOMI_CLIENT_ID is',
|
|
52
|
+
" written under the target template's own convention, e.g. NEXT_PUBLIC_RAKOMI_CLIENT_ID.",
|
|
53
|
+
' Your tenant signup auto-provisions a default OAuth client for you — find its Client ID in',
|
|
54
|
+
' your Rakomi dashboard: Settings -> Development credentials.)',
|
|
49
55
|
'',
|
|
50
56
|
`After scaffolding, the next-step walkthrough lives at ${PORTAL_HOST}/quickstart/<slug>.`,
|
|
51
57
|
'',
|
|
@@ -60,18 +66,26 @@ export function usageLine() {
|
|
|
60
66
|
* manager-aware command list. The install step is framed as the user's own next step that
|
|
61
67
|
* depends on the public npm registry, not a guarantee. No color is emitted (NO_COLOR-safe by
|
|
62
68
|
* construction) and the copy is stack-neutral so a tutorial can quote it verbatim.
|
|
69
|
+
*
|
|
70
|
+
* `mcpConfigWritten` (default `true`) reports whether `.mcp.json` + `AGENTS.md` were scaffolded
|
|
71
|
+
* into the new project (they are, unless `--no-mcp` was passed) — when they were, one extra line
|
|
72
|
+
* tells the user the project is already agent-ready and what to run to finish sign-in, plus a
|
|
73
|
+
* pointer to AGENTS.md for a fuller briefing.
|
|
63
74
|
*/
|
|
64
|
-
export function postInstallMessage(slug, directory, pm) {
|
|
65
|
-
|
|
75
|
+
export function postInstallMessage(slug, directory, pm, mcpConfigWritten = true) {
|
|
76
|
+
const lines = [
|
|
66
77
|
'',
|
|
67
78
|
`Done. Your Rakomi ${slug} app is ready in ${directory}`,
|
|
68
79
|
'',
|
|
69
80
|
'Next steps:',
|
|
70
81
|
` 1. cd ${directory}`,
|
|
71
|
-
` 2. ${installCommand(pm)} (installs
|
|
82
|
+
` 2. ${installCommand(pm)} (installs the Rakomi SDK from the public npm registry)`,
|
|
72
83
|
` 3. ${runCommand(pm)}`,
|
|
73
84
|
'',
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
];
|
|
86
|
+
if (mcpConfigWritten) {
|
|
87
|
+
lines.push('Agent-ready: .mcp.json points Claude Code at the Rakomi MCP server — run `claude mcp login rakomi` to finish sign-in.', 'See AGENTS.md for the full briefing any AI agent working in this project can read.', '');
|
|
88
|
+
}
|
|
89
|
+
lines.push(`Walkthrough: ${portalUrl(slug)}`, '');
|
|
90
|
+
return lines.join('\n');
|
|
77
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-rakomi-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scaffold a Rakomi quickstart app. EU-native auth-as-a-service. npx create-rakomi-app --template <slug>",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rakomi",
|
|
@@ -43,6 +43,6 @@
|
|
|
43
43
|
"build": "tsc",
|
|
44
44
|
"typecheck": "tsc --noEmit",
|
|
45
45
|
"lint": "eslint src/ test/ --max-warnings=0",
|
|
46
|
-
"test": "vitest run"
|
|
46
|
+
"test": "node ../../scripts/assert-not-story-agent-worktree.mjs test && node ../../scripts/ci/vitest-fork-crash-retry.mjs --results ./test-results.json -- vitest run"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/sbom.cdx.json
CHANGED
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
},
|
|
27
27
|
"component": {
|
|
28
28
|
"type": "library",
|
|
29
|
-
"bom-ref": "pkg:npm/create-rakomi-app@0.
|
|
29
|
+
"bom-ref": "pkg:npm/create-rakomi-app@0.2.0",
|
|
30
30
|
"name": "create-rakomi-app",
|
|
31
|
-
"version": "0.
|
|
32
|
-
"purl": "pkg:npm/create-rakomi-app@0.
|
|
31
|
+
"version": "0.2.0",
|
|
32
|
+
"purl": "pkg:npm/create-rakomi-app@0.2.0",
|
|
33
33
|
"licenses": [
|
|
34
34
|
{
|
|
35
35
|
"license": {
|