@layers/amba 1.0.1 → 1.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.
@@ -14,3 +14,14 @@ export interface ProjectConfig {
14
14
  apiUrl: string;
15
15
  }
16
16
  export declare function loadProjectConfig(cwd?: string): Promise<ProjectConfig>;
17
+ /**
18
+ * Parse a `.env`-style file body into a flat string map.
19
+ *
20
+ * Lines are trimmed; blank lines and `#`-comments are skipped; lines
21
+ * without an `=` are skipped. Values may be wrapped in matching single
22
+ * or double quotes which are stripped on read. Bug fixes should land
23
+ * here once — both `project-config.ts` (resolves `AMBA_PROJECT_ID`)
24
+ * and `commands/functions.ts` (loads `.env.local` for the local dev
25
+ * server) call this.
26
+ */
27
+ export declare function parseEnv(content: string): Record<string, string>;
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Headless agentic sandbox bootstrap.
3
+ *
4
+ * Implements `amba init --sandbox`: the zero-question, no-browser path
5
+ * an AI coding agent runs when a developer pastes the homepage prompt:
6
+ *
7
+ * Run `npx @layers/amba init --sandbox` and follow the
8
+ * instructions it prints.
9
+ *
10
+ * The CLI does everything: synthesize an anonymous email + password,
11
+ * sign the developer up via the public `/v1/auth/developer/signup`
12
+ * endpoint (no Bearer needed), pluck the returned PAT + project
13
+ * credentials, and write them into:
14
+ *
15
+ * - `~/.amba/credentials.json` (chmod 0600)
16
+ * - `<cwd>/.env.local` (.gitignored — SDK reads it)
17
+ * - `<cwd>/AMBA.md` (markdown context for the agent)
18
+ * - every detected MCP client config (`~/.claude.json`,
19
+ * `~/.cursor/mcp.json`, `~/.codeium/windsurf/mcp_config.json`,
20
+ * plus their project-local equivalents WHEN already present —
21
+ * never created from scratch, to avoid cluttering repos)
22
+ *
23
+ * The MCP-config merge is non-destructive: an existing `mcpServers.amba`
24
+ * entry is replaced with the new PAT, but the prior file is copied
25
+ * aside to `<path>.bak-<unix-ms>` first so a developer who had a real
26
+ * production PAT wired in can recover. Every other server entry is
27
+ * preserved. JSON files that already exist but lack an `mcpServers`
28
+ * key gain one; missing files for the global locations get scaffolded
29
+ * with a minimal `{ "mcpServers": { "amba": … }}`.
30
+ *
31
+ * Similarly, a pre-existing `~/.amba/credentials.json` whose `source`
32
+ * is not `'sandbox-init'` is backed up to `credentials.json.bak-<ms>`
33
+ * before the sandbox PAT replaces it. Idempotent re-runs from our own
34
+ * sandbox session do NOT trigger a backup.
35
+ *
36
+ * Provisioning polling: deliberately skipped. The server returns the
37
+ * project row with `provisioning_status: 'provisioning'` immediately and
38
+ * the workflow flips it to `'active'` within ~5s. The agent's next SDK
39
+ * call may briefly retry — fine. Blocking the CLI here would just hide
40
+ * the same wait behind a different progress indicator.
41
+ */
42
+ export interface SandboxSignupRequest {
43
+ email: string;
44
+ password: string;
45
+ name?: string;
46
+ }
47
+ /**
48
+ * Shape returned by `POST /v1/auth/developer/signup`. Only the fields the
49
+ * sandbox flow actually consumes are typed — the response body has more
50
+ * (developer row, token expiry, etc.) but we don't need them here.
51
+ */
52
+ export interface SandboxSignupResponse {
53
+ pat: string;
54
+ project_id: string;
55
+ client_key: string;
56
+ server_key?: string;
57
+ api_url: string;
58
+ /** `'provisioning'` immediately after signup; flips to `'active'` once the workflow finishes. */
59
+ provisioning_status?: string;
60
+ /** Email-verification URL — surfaced by the API so we can echo it for upgrade. */
61
+ verify_url?: string;
62
+ /** The actual email we used (may be the auto-generated one). */
63
+ email: string;
64
+ }
65
+ export interface McpClientConfigTarget {
66
+ /** Display name for logging. */
67
+ label: string;
68
+ /** Absolute path on disk. */
69
+ path: string;
70
+ /** Whether to create the file if it doesn't exist. Global configs: yes. Project-local: no. */
71
+ scaffoldIfMissing: boolean;
72
+ }
73
+ export interface SandboxResult {
74
+ email: string;
75
+ projectId: string;
76
+ pat: string;
77
+ patPreview: string;
78
+ clientKey: string;
79
+ /** The exact API URL the signup POST hit. Source of truth for callers. */
80
+ apiUrl: string;
81
+ credentialsPath: string;
82
+ /** Backup of a pre-existing non-sandbox `~/.amba/credentials.json`, or null. */
83
+ credentialsBackedUpTo: string | null;
84
+ envLocalPath: string;
85
+ ambaMdPath: string;
86
+ /** One record per MCP client config we touched; includes per-file backup info. */
87
+ mcpConfigsWritten: McpConfigPathResult[];
88
+ /** Best-guess SDK package for the framework detected in CWD. */
89
+ sdkPackage: string;
90
+ framework: string;
91
+ verifyUrl: string | null;
92
+ provisioningStatus: string;
93
+ /**
94
+ * Absolute path of the `.claude/skills/amba-build/SKILL.md` we wrote
95
+ * during the sandbox init, or `null` if the skill install was skipped
96
+ * (e.g. `--no-skills`).
97
+ */
98
+ skillPath: string | null;
99
+ }
100
+ /**
101
+ * Generate a deterministic-looking but globally-unique sandbox email.
102
+ *
103
+ * Pattern: `sandbox-<epoch>-<6char>@layers.com`.
104
+ *
105
+ * The control DB's `developers` table has a UNIQUE(email) constraint and
106
+ * a 5-per-minute / 50-per-day per-IP rate limit on signup. Embedding the
107
+ * epoch + a 6-char nonce keeps the collision probability negligible even
108
+ * across a herd of CI agents all running `amba init --sandbox` from the
109
+ * same VPC.
110
+ *
111
+ * We use `@layers.com` (not the customer's own domain) because the
112
+ * sandbox tier is pre-verification — the developer never receives or
113
+ * actions a verification email for this address. When they want to
114
+ * upgrade, the CLI prints the verify URL the API returned so they can
115
+ * claim a real email in the console.
116
+ */
117
+ export declare function generateSandboxEmail(): string;
118
+ /**
119
+ * Random URL-safe password. 24 raw bytes → 32 base64url chars; well over
120
+ * the 8-char minimum the API enforces, with ~192 bits of entropy.
121
+ *
122
+ * The password is never shown to the developer or written anywhere — the
123
+ * PAT is what gets stored. We generate it solely because `POST /signup`
124
+ * requires it (and demands a non-empty value); a future API change could
125
+ * accept "agent signup" with no password and we'd drop this entirely.
126
+ */
127
+ export declare function generateSandboxPassword(): string;
128
+ /**
129
+ * POST the synthesized credentials at the public signup endpoint.
130
+ *
131
+ * No Bearer auth — this is the bootstrap call that mints one. We use
132
+ * `fetch` directly (not the api-client wrapper) because that wrapper
133
+ * always resolves a bearer token first, which is exactly what we don't
134
+ * have yet.
135
+ *
136
+ * Returns the unwrapped, flattened shape consumed by the rest of the
137
+ * sandbox flow. Throws with a human-readable message on any non-2xx so
138
+ * the CLI's `runAction` wrapper can surface it without crashing on a
139
+ * generic 'fetch failed'.
140
+ */
141
+ export declare function performSandboxSignup(req: SandboxSignupRequest, options?: {
142
+ apiUrl?: string;
143
+ fetchImpl?: typeof fetch;
144
+ }): Promise<SandboxSignupResponse>;
145
+ /**
146
+ * Result of writing sandbox credentials. The optional `backedUpTo` lets
147
+ * the CLI surface a "we moved your existing creds aside" notice so a
148
+ * developer who accidentally ran `--sandbox` on top of a real OAuth
149
+ * session can recover.
150
+ */
151
+ export interface WriteCredentialsResult {
152
+ path: string;
153
+ /** Absolute path of the backup file, or null if no backup was made. */
154
+ backedUpTo: string | null;
155
+ }
156
+ /**
157
+ * Write the PAT to `~/.amba/credentials.json` (chmod 0600) in a shape
158
+ * the existing `loadCredentials` reader recognises.
159
+ *
160
+ * `auth.ts` was built around browser-OAuth tokens (`access_token` +
161
+ * `refresh_token` + `expires_at`). PATs are long-lived and don't refresh
162
+ * — but the stored-creds reader only inspects `access_token`, so we
163
+ * write the PAT there and leave `refresh_token` empty + a far-future
164
+ * `expires_at` so the expiry guard never fires.
165
+ *
166
+ * Real-credential safety: if the file already exists AND its `source`
167
+ * is NOT `'sandbox-init'` AND `access_token` is non-empty, we treat it
168
+ * as a real OAuth/PAT session and back it up to
169
+ * `credentials.json.bak-<unix-ms>` before overwriting. The next
170
+ * `--sandbox` run reuses our own previous sandbox creds without
171
+ * back-up. This keeps the agentic flow idempotent while preventing a
172
+ * silent clobber of a developer's real account.
173
+ */
174
+ export declare function writeSandboxCredentials(pat: string, options?: {
175
+ homeDir?: string;
176
+ }): Promise<WriteCredentialsResult>;
177
+ /**
178
+ * Write or update `<cwd>/.env.local` with the sandbox project's keys.
179
+ *
180
+ * Mirrors the `init` interactive flow exactly so the existing env-read
181
+ * conventions in the SDKs and CLI commands keep working. The merge
182
+ * logic: if the file already exists and contains an `AMBA_PROJECT_ID`
183
+ * line we replace the three Amba lines in place; otherwise we append a
184
+ * fresh stanza.
185
+ */
186
+ export declare function writeSandboxEnvLocal(cwd: string, projectId: string, clientKey: string, apiUrl: string): Promise<string>;
187
+ /**
188
+ * Write the AMBA.md sandbox-tier guide to `<cwd>/AMBA.md`.
189
+ *
190
+ * Always overwrites — the file is meant to be regenerated, and the
191
+ * interactive `init` flow's longer AMBA.md template is replaced here
192
+ * with a sandbox-specific shorter one (with upgrade instructions).
193
+ */
194
+ export declare function writeSandboxAmbaMd(cwd: string, ctx: {
195
+ projectId: string;
196
+ email: string;
197
+ verifyUrl: string | null;
198
+ sdkPackage: string;
199
+ framework: string;
200
+ apiUrl: string;
201
+ }): Promise<string>;
202
+ /**
203
+ * The canonical Amba MCP entry. Used as the value of
204
+ * `mcpServers.amba` in every detected client config.
205
+ *
206
+ * Shape note: Claude Code, Cursor, and Windsurf all read the same
207
+ * `type` + `url` + `headers` triple. (Windsurf historically also
208
+ * accepted `serverUrl` — we emit `url` to match the modern shape it
209
+ * also accepts, and skip emitting the legacy alias to keep the JSON
210
+ * minimal.)
211
+ */
212
+ export declare function buildAmbaMcpEntry(pat: string): Record<string, unknown>;
213
+ /**
214
+ * Identifier for the MCP client family a config file belongs to.
215
+ * Used by the CLI's done-message to print per-client restart bullets
216
+ * — and only for clients whose config we actually wrote to.
217
+ */
218
+ export type McpClientKind = 'claude-code' | 'cursor' | 'windsurf';
219
+ /**
220
+ * Map an MCP config file path back to its client family. Returns null
221
+ * for paths that don't match any known config location — defensive
222
+ * against future additions to `mcpClientTargets`.
223
+ *
224
+ * The match is on path tail rather than full equality so the cwd /
225
+ * homedir-injected variants both classify correctly. We deliberately
226
+ * accept both global and project-local Claude Code paths
227
+ * (`.claude.json` and `.mcp.json`) as 'claude-code'.
228
+ */
229
+ export declare function classifyMcpPath(path: string): McpClientKind | null;
230
+ /**
231
+ * Reduce a list of written-config paths to the set of unique client
232
+ * families they belong to. Order: claude-code, cursor, windsurf (so
233
+ * the done-message renders consistently). Skips unclassified paths
234
+ * silently.
235
+ */
236
+ export declare function clientKindsFromPaths(paths: string[]): McpClientKind[];
237
+ /**
238
+ * The list of MCP client config files we probe. Order matters only for
239
+ * the printed report.
240
+ *
241
+ * Project-local entries are listed but only get touched when the file
242
+ * already exists in CWD — we don't want to scatter `.mcp.json` /
243
+ * `.cursor/mcp.json` files into random user repos that have never been
244
+ * MCP-configured.
245
+ */
246
+ export declare function mcpClientTargets(cwd: string, options?: {
247
+ homeDir?: string;
248
+ }): McpClientConfigTarget[];
249
+ /**
250
+ * Per-target result of an MCP config merge.
251
+ *
252
+ * - `path: null` means the target file didn't exist and scaffolding was
253
+ * not allowed (project-local targets in unconfigured repos).
254
+ * - `backedUpTo` is the absolute path of the backup file when an
255
+ * existing `mcpServers.amba` entry was present (so a developer who
256
+ * had a real PAT wired in can recover); `null` when no backup was
257
+ * needed.
258
+ */
259
+ export interface McpConfigWriteResult {
260
+ path: string | null;
261
+ backedUpTo: string | null;
262
+ }
263
+ /**
264
+ * Merge the `amba` entry into a single client config file.
265
+ *
266
+ * Strategy:
267
+ * - If the file exists, load + JSON-parse. If parse fails, throw with
268
+ * a clear "we won't clobber malformed JSON" error.
269
+ * - If the file doesn't exist and scaffolding is allowed, create the
270
+ * parent dir and write `{ "mcpServers": { "amba": ... } }`.
271
+ * - In all cases, `mcpServers.amba` ends up set; every other
272
+ * `mcpServers.*` entry is preserved.
273
+ *
274
+ * Real-credential safety: if the existing file ALREADY has a
275
+ * `mcpServers.amba` entry, we copy the whole file aside to
276
+ * `<path>.bak-<unix-ms>` BEFORE merging. This protects a developer who
277
+ * had a real production PAT wired in and then ran `amba init --sandbox`
278
+ * to "try" the flow. Idempotent re-runs from the same sandbox session
279
+ * still trigger a backup — cheap, and the developer can `rm *.bak-*`
280
+ * any time.
281
+ */
282
+ export declare function mergeMcpConfigFile(target: McpClientConfigTarget, pat: string): Promise<McpConfigWriteResult>;
283
+ /**
284
+ * Per-target result returned by `writeAllMcpConfigs`. Same shape as
285
+ * `McpConfigWriteResult` but always carries a non-null `path` (skipped
286
+ * targets aren't included in the list).
287
+ */
288
+ export interface McpConfigPathResult {
289
+ path: string;
290
+ backedUpTo: string | null;
291
+ }
292
+ /**
293
+ * Probe every known MCP client location and merge our entry into the
294
+ * ones that exist (or that are flagged scaffoldIfMissing). Returns one
295
+ * record per touched file (skipped files are omitted).
296
+ *
297
+ * `warn` is invoked (instead of `console.warn`) for per-target errors
298
+ * so callers using `--json` can route those notices to stderr and keep
299
+ * stdout machine-parseable.
300
+ */
301
+ export declare function writeAllMcpConfigs(cwd: string, pat: string, options?: {
302
+ homeDir?: string;
303
+ warn?: (msg: string) => void;
304
+ }): Promise<McpConfigPathResult[]>;
305
+ /**
306
+ * Render the canonical Amba MCP snippet for clients we can't auto-wire
307
+ * (anything outside the {Claude Code, Cursor, Windsurf} set). Printed by
308
+ * the CLI when no client configs are detected so the developer at least
309
+ * has a paste-ready JSON blob.
310
+ */
311
+ export declare function formatManualMcpSnippet(pat: string): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Claude Code skill installer for `amba init --sandbox`.
3
+ *
4
+ * Writes a project-local `.claude/skills/amba-build/SKILL.md` file
5
+ * containing the canonical "/goal" prompt for building a full Expo
6
+ * app with Amba as the only backend. Two behaviors:
7
+ *
8
+ * 1. **Live fetch first.** The skill's runtime instructions tell
9
+ * Claude Code to `curl` the live MDX from
10
+ * `https://docs.amba.dev/docs/prompts/expo-build.md`. So as long
11
+ * as docs is reachable, the agent always uses the latest version.
12
+ *
13
+ * 2. **Inlined snapshot fallback.** The same SKILL.md file embeds a
14
+ * verbatim copy of the prompt body — captured at CLI install
15
+ * time from `@layers/amba-mcp/prompts`. Offline agents (or ones
16
+ * whose curl 404s during the docs deploy gap) still get a
17
+ * usable prompt.
18
+ *
19
+ * Out of scope (per DX-16): Cursor / Windsurf shortcut files. Those
20
+ * editors don't ingest Claude Code skills; their users paste the
21
+ * URL directly. The CLI's success line still surfaces the
22
+ * `/amba-build` command + the docs URL so both audiences are served.
23
+ *
24
+ * Project-local install is the default because skills written into
25
+ * `~/.claude/skills/` are user-global and would persist across
26
+ * unrelated projects (and accumulate stale copies of the inlined
27
+ * snapshot from old `amba init` runs). A project-scoped install
28
+ * disappears when the user `rm -rf`s the project.
29
+ */
30
+ export interface WriteAmbaBuildSkillOptions {
31
+ /**
32
+ * Where to root the `.claude/skills/amba-build/` directory.
33
+ * Defaults to `cwd` — project-local. Override only in tests (the
34
+ * production caller never passes anything else).
35
+ */
36
+ baseDir?: string;
37
+ }
38
+ export interface WriteAmbaBuildSkillResult {
39
+ /** Absolute path of the SKILL.md we wrote. */
40
+ path: string;
41
+ }
42
+ /**
43
+ * Build the contents of `.claude/skills/amba-build/SKILL.md`.
44
+ *
45
+ * Exported as a pure function so the unit tests can assert structural
46
+ * properties (frontmatter, fetcher block, inlined snapshot fence)
47
+ * without round-tripping through the filesystem.
48
+ *
49
+ * Structure:
50
+ * 1. YAML frontmatter — `description` so Claude Code's skill
51
+ * indexer picks it up.
52
+ * 2. Skill body — invocation instructions, fetcher one-liner,
53
+ * fallback rule.
54
+ * 3. Inlined snapshot — fenced code block containing the
55
+ * EXPO_BUILD_PROMPT_MD body verbatim. The snapshot is bounded
56
+ * by a marker comment so a future `amba update-skills` command
57
+ * can find and refresh just the inlined region without
58
+ * clobbering user customizations above it.
59
+ */
60
+ export declare function buildAmbaBuildSkillContent(): string;
61
+ /**
62
+ * Write `.claude/skills/amba-build/SKILL.md` into the target project.
63
+ *
64
+ * Always overwrites — the skill file is meant to be regenerated each
65
+ * time `amba init --sandbox` runs (so the inlined snapshot stays
66
+ * fresh). Anything the user customized above the snapshot markers
67
+ * would be lost on a re-init; that's an accepted trade-off for the
68
+ * agentic single-command flow.
69
+ *
70
+ * If a future need for "preserve user edits across re-init" surfaces,
71
+ * the right shape is a separate `amba update-skills` command that
72
+ * surgically rewrites the inlined-snapshot region only — leaving the
73
+ * surrounding text untouched. Out of scope for DX-16.
74
+ */
75
+ export declare function writeAmbaBuildSkill(options?: WriteAmbaBuildSkillOptions): Promise<WriteAmbaBuildSkillResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "amba — agent-native backend-as-a-service. Functions, collections, storage, AI, email, queues, sites — one CLI to spin up your project and ship to production. `npx @layers/amba init` to start.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,12 +46,15 @@
46
46
  "devDependencies": {
47
47
  "@types/node": "^22.10.2",
48
48
  "tsdown": "^0.12.5",
49
- "typescript": "^5.8.3"
49
+ "typescript": "^5.8.3",
50
+ "vitest": "^3.2.4",
51
+ "@layers/amba-mcp": "1.0.1"
50
52
  },
51
53
  "scripts": {
52
54
  "build": "tsdown && tsc --emitDeclarationOnly",
53
55
  "dev": "tsdown --watch",
54
56
  "typecheck": "tsc --noEmit",
57
+ "test": "vitest run",
55
58
  "clean": "rm -rf dist"
56
59
  }
57
60
  }