@layers/amba 4.0.4 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `amba ship` config — the typed, hand-validated launch config.
3
+ *
4
+ * One file per repo (default `amba.ship.json`) describes how to take this Expo
5
+ * app from code-complete to live on the App Store and Google Play. It carries
6
+ * NON-SECRET identifiers and the NAME of the env var holding the Expo token —
7
+ * never secret values (the App Store Connect `.p8` and Play service-account
8
+ * JSON live in `eas.json`, owned by the app repo, exactly where the build runs).
9
+ *
10
+ * Validation is hand-rolled (the CLI deliberately ships no `zod` — see
11
+ * `_internal/shared.ts` for the same return-all-errors-at-once pattern). The
12
+ * field names are the canonical contract; `validateShipConfig` reports every
13
+ * problem in one pass and enforces the cross-field invariants.
14
+ *
15
+ * Monetization is NOT described here: the App Store / Play product catalog is
16
+ * Amba's monetization control plane (`amba monetization`, the durable Track H
17
+ * apply against the provider). `ship` only flips `monetization.enabled` to
18
+ * decide whether to verify that catalog is in sync before building.
19
+ */
20
+ /** The ordered launch phases. `ship` runs them in this sequence. */
21
+ export declare const SHIP_PHASES: readonly ["preflight", "monetization", "build", "submit", "metadata", "release"];
22
+ export type Phase = (typeof SHIP_PHASES)[number];
23
+ export declare const SHIP_PLATFORMS: readonly ["ios", "android"];
24
+ export type Platform = (typeof SHIP_PLATFORMS)[number];
25
+ /** Google Play release tracks `ship` can submit to. */
26
+ export declare const PLAY_TRACKS: readonly ["internal", "alpha", "beta", "production"];
27
+ export type PlayTrack = (typeof PLAY_TRACKS)[number];
28
+ /** Default config filename when no `--config <path>` is passed. */
29
+ export declare const DEFAULT_SHIP_CONFIG_FILE = "amba.ship.json";
30
+ export interface ShipConfig {
31
+ /** Schema version of this config file (forward-compatible migrations). */
32
+ version: number;
33
+ app: {
34
+ /** Lowercase key used to scope the per-app launch state file. */
35
+ slug: string;
36
+ /** Human-facing app name (App Store / Play display). */
37
+ name: string;
38
+ /** Marketing version MAJOR.MINOR.PATCH. */
39
+ version: string;
40
+ /** iOS bundle identifier (reverse-DNS). */
41
+ bundleId: string;
42
+ /** Android application id (reverse-DNS); may equal bundleId. */
43
+ androidPackage: string;
44
+ };
45
+ build: {
46
+ /** EAS build profile (a `build.<profile>` key in eas.json), e.g. "production". */
47
+ profile: string;
48
+ /** Platforms to build/submit/release. At least one. */
49
+ platforms: Platform[];
50
+ };
51
+ ios: {
52
+ /**
53
+ * App Store Connect numeric app id. `null` until the app record exists.
54
+ * EAS Submit cannot create the record (and under --non-interactive cannot
55
+ * prompt for it, eas-cli #2418), so `ship` treats a null value as a manual
56
+ * gate on the submit phase.
57
+ */
58
+ ascAppId: string | null;
59
+ };
60
+ android: {
61
+ /** Track the submit phase lands on; release promotes it to production. */
62
+ track: PlayTrack;
63
+ };
64
+ monetization: {
65
+ /**
66
+ * When true, `ship` verifies the App Store / Play product catalog is in
67
+ * sync with the Amba monetization control plane before building, and gates
68
+ * if it has drifted (run `amba monetization apply`). When false (the
69
+ * default for apps that don't sell anything), the monetization phase is a
70
+ * no-op skip.
71
+ */
72
+ enabled: boolean;
73
+ };
74
+ secrets: {
75
+ /** NAME of the env var holding the Expo token for non-interactive EAS auth. */
76
+ expoTokenEnvVar: string;
77
+ };
78
+ }
79
+ export type ValidateResult = {
80
+ ok: true;
81
+ config: ShipConfig;
82
+ } | {
83
+ ok: false;
84
+ errors: string[];
85
+ };
86
+ /**
87
+ * Validate an untrusted parsed JSON value into a typed `ShipConfig`,
88
+ * collecting EVERY problem (not just the first) so a misconfigured file
89
+ * surfaces all its issues in one run. Returns the typed config on success.
90
+ */
91
+ export declare function validateShipConfig(raw: unknown): ValidateResult;
92
+ /**
93
+ * A starter config written by `amba ship init`. Placeholders are intentionally
94
+ * obvious so the validator's invariants don't pass on an unedited file's IDs
95
+ * (the reverse-DNS / semver placeholders DO validate so `--phase preflight`
96
+ * works immediately; the operator swaps in their real values).
97
+ */
98
+ export declare function shipConfigTemplate(slug: string): ShipConfig;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The external-command boundary for `amba ship`.
3
+ *
4
+ * Every shell-out (always `eas`) goes through an `Executor`, so the phase
5
+ * logic — which args get built, how `--json` output is parsed, which failures
6
+ * map to which manual gate — is unit-tested against a fake executor with NO
7
+ * live cloud, no EAS account, and no Apple/Google credentials. The default
8
+ * `createNodeExecutor()` is a thin, shell-free wrapper around
9
+ * `node:child_process.spawn` (mirrors the launch toolkit's `lib/exec`).
10
+ */
11
+ export interface ExecRunOptions {
12
+ /** Extra environment variables merged over process.env. */
13
+ env?: NodeJS.ProcessEnv;
14
+ /** Working directory for the child. Defaults to process.cwd(). */
15
+ cwd?: string;
16
+ /** Hard timeout in ms. On expiry the child is SIGKILLed and the call rejects. */
17
+ timeoutMs?: number;
18
+ /** If true, stream child stdout/stderr live to this process. Default false. */
19
+ stream?: boolean;
20
+ /** If true, log the command and return a synthetic success WITHOUT running. */
21
+ dryRun?: boolean;
22
+ }
23
+ export interface ExecResult {
24
+ /** The redacted command line, for logging / state. */
25
+ command: string;
26
+ code: number;
27
+ stdout: string;
28
+ stderr: string;
29
+ /** True when the call was skipped because of dry-run. */
30
+ dryRun: boolean;
31
+ }
32
+ export interface Executor {
33
+ /** Run a command. Resolves on exit 0; REJECTS (with an actionable message) otherwise. */
34
+ run(cmd: string, args: string[], opts?: ExecRunOptions): Promise<ExecResult>;
35
+ }
36
+ /**
37
+ * Redact secret-shaped tokens from a string before it is logged. A backstop,
38
+ * not a license — `ship` never puts secret values on a command line (EAS reads
39
+ * its token from the env and its store creds from eas.json). Covers Expo /
40
+ * common access-token shapes and `Bearer`/`sk_` prefixes.
41
+ */
42
+ export declare function redactSecrets(text: string): string;
43
+ /** A backing executor over `node:child_process.spawn`. Never invokes a shell. */
44
+ export declare function createNodeExecutor(globalDryRun?: boolean): Executor;
45
+ /**
46
+ * Run a command and report success/failure WITHOUT throwing — for read-only
47
+ * "is this CLI installed/authed?" probes where a non-zero exit is data, not a
48
+ * fatal error.
49
+ */
50
+ export declare function probe(exec: Executor, cmd: string, args: string[], opts?: ExecRunOptions): Promise<{
51
+ ok: boolean;
52
+ result?: ExecResult;
53
+ }>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The `amba ship` orchestrator.
3
+ *
4
+ * Sequences the requested phases in canonical order, honoring per-app
5
+ * idempotency state (skip phases already `done` unless `--force`), propagating
6
+ * a global `--dry-run` to every phase, and stopping the chain at the first gate
7
+ * or failure (later phases depend on earlier ones — the "clear a gate → re-run"
8
+ * loop). It owns ALL state writes and the clock; the phase runners stay pure of
9
+ * both. Returns structured rows for the command layer to render.
10
+ */
11
+ import type { Phase, Platform, ShipConfig } from './config.js';
12
+ import type { Executor } from './exec.js';
13
+ import { type PhaseRunner } from './phases.js';
14
+ export type RowStatus = 'done' | 'skipped' | 'gate' | 'failed' | 'pending' | 'dry-run';
15
+ export interface ShipRow {
16
+ phase: Phase;
17
+ status: RowStatus;
18
+ detail: string;
19
+ gates?: string[];
20
+ }
21
+ export interface ShipResult {
22
+ rows: ShipRow[];
23
+ /** A gate stopped the chain (human store action needed) — not an error. */
24
+ blocked: boolean;
25
+ /** A phase errored. */
26
+ failed: boolean;
27
+ }
28
+ export interface ShipRunOptions {
29
+ config: ShipConfig;
30
+ /** Which phases to run (any subset of SHIP_PHASES). */
31
+ phases: Phase[];
32
+ platforms: Platform[];
33
+ exec: Executor;
34
+ dryRun: boolean;
35
+ force: boolean;
36
+ stateBaseDir?: string;
37
+ env?: NodeJS.ProcessEnv;
38
+ now?: () => string;
39
+ log?: (line: string) => void;
40
+ sleep?: (ms: number) => Promise<void>;
41
+ /** Resolve the linked Amba project id (default: from .env.local). */
42
+ resolveProjectId?: () => Promise<string | null>;
43
+ /** Admin GET (default: the real api-client). Injected in tests. */
44
+ adminGet?: <T>(path: string) => Promise<{
45
+ data: T;
46
+ }>;
47
+ /** Phase runners (default: the real registry). Injected in tests. */
48
+ runners?: Record<Phase, PhaseRunner>;
49
+ releasePollMs?: number;
50
+ releaseMaxWaitMs?: number;
51
+ }
52
+ export declare function runShip(opts: ShipRunOptions): Promise<ShipResult>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The six `amba ship` phases.
3
+ *
4
+ * Each runner receives a `ShipContext` (config + an injected `Executor` for
5
+ * `eas`, an injected admin-API `adminGet`, a project resolver, and state
6
+ * accessors) and returns a `PhaseOutcome`. The cloud-touching work is all
7
+ * behind those injection points, so the arg-building, `--json` parsing, and
8
+ * failure→manual-gate mapping below are unit-tested with no live cloud.
9
+ *
10
+ * Honest gates: several launch steps are NOT API-automatable (create the App
11
+ * Store Connect / Play app record, the Apple Paid-Apps agreement, screenshots,
12
+ * store review). A runner DETECTS such a step and returns `status: 'gate'` with
13
+ * the exact action + console URL, rather than pretending it is automated.
14
+ */
15
+ import type { Executor } from './exec.js';
16
+ import type { Phase, Platform, ShipConfig } from './config.js';
17
+ import type { PhaseStatus } from './state.js';
18
+ export interface ShipContext {
19
+ config: ShipConfig;
20
+ exec: Executor;
21
+ platforms: Platform[];
22
+ dryRun: boolean;
23
+ force: boolean;
24
+ env: NodeJS.ProcessEnv;
25
+ now: () => string;
26
+ log: (line: string) => void;
27
+ /** Read a phase's persisted note (a JSON string) or null. */
28
+ getNote: (phase: Phase) => string | null;
29
+ /** Checkpoint a phase's status + note mid-run. No-op under dryRun. */
30
+ checkpoint: (phase: Phase, status: PhaseStatus, note: string) => void;
31
+ /** Resolve the linked Amba project id, or null when no project is linked. */
32
+ resolveProjectId: () => Promise<string | null>;
33
+ /** GET an admin API path → `{ data }`. */
34
+ adminGet: <T>(path: string) => Promise<{
35
+ data: T;
36
+ }>;
37
+ /** Sleep helper (injected so release-polling is instant in tests). */
38
+ sleep: (ms: number) => Promise<void>;
39
+ /** Build-completion poll cadence (release phase). Small values in tests. */
40
+ releasePollMs: number;
41
+ releaseMaxWaitMs: number;
42
+ }
43
+ export interface PhaseOutcome {
44
+ status: 'done' | 'gate' | 'failed' | 'skipped';
45
+ detail: string;
46
+ /** Structured JSON note to persist (build/submit/release maps). */
47
+ note?: string;
48
+ /** Human-action items when status === 'gate' (printed prominently). */
49
+ gates?: string[];
50
+ }
51
+ export type PhaseRunner = (ctx: ShipContext) => Promise<PhaseOutcome>;
52
+ export declare function easBuildArgs(platform: Platform, profile: string): string[];
53
+ export declare function easSubmitArgs(input: {
54
+ platform: Platform;
55
+ profile: string;
56
+ buildId: string | null;
57
+ track?: string;
58
+ }): string[];
59
+ export declare function easBuildViewArgs(buildId: string): string[];
60
+ export declare function easMetadataArgs(profile: string): string[];
61
+ export interface EasBuild {
62
+ id: string;
63
+ status?: string;
64
+ platform?: string;
65
+ appVersion?: string;
66
+ artifacts?: {
67
+ applicationArchiveUrl?: string;
68
+ buildUrl?: string;
69
+ };
70
+ buildDetailsPageUrl?: string;
71
+ }
72
+ export interface EasSubmission {
73
+ id?: string;
74
+ status?: string;
75
+ platform?: string;
76
+ buildId?: string;
77
+ submissionDetailsPageUrl?: string;
78
+ }
79
+ /** Pull the build object for `platform` out of `eas build --json` stdout. */
80
+ export declare function extractBuild(raw: string, platform: Platform): EasBuild;
81
+ export declare function artifactUrlOf(b: EasBuild): string | null;
82
+ /** Parse the submission object for `platform` out of `eas submit --json` stdout. */
83
+ export declare function parseSubmission(raw: string, platform: Platform): EasSubmission;
84
+ export declare function diagnoseBuildFailure(message: string): string;
85
+ /** A submit failure that needs a human store action, or null for a plain error. */
86
+ export declare function diagnoseSubmitGate(platform: Platform, message: string): string | null;
87
+ export declare const PHASE_RUNNERS: Record<Phase, PhaseRunner>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `amba ship` per-app idempotency state.
3
+ *
4
+ * Each app's launch progress is a JSON file at `.amba/ship-state/<slug>.json`
5
+ * recording, per phase, whether it is done / failed / blocked-by-a-human-gate /
6
+ * in-progress, when, and an optional structured `note` (e.g. the build ids a
7
+ * later phase reads back). The orchestrator uses it to skip already-done phases
8
+ * and to resume the "clear a gate → re-run" loop.
9
+ *
10
+ * PURITY RULE: this module NEVER reads the clock itself — callers pass an
11
+ * ISO-8601 timestamp into every mutation so it stays deterministically
12
+ * testable (the orchestrator owns the clock). The file is purely an
13
+ * optimization/audit log: deleting it is safe because every phase re-detects
14
+ * real cloud state before mutating.
15
+ */
16
+ import type { Phase } from './config.js';
17
+ export type PhaseStatus = 'pending' | 'in-progress' | 'done' | 'failed' | 'gate';
18
+ export interface PhaseRecord {
19
+ status: PhaseStatus;
20
+ /** ISO timestamp of the last status change (supplied by the caller). */
21
+ updatedAt: string;
22
+ /** Free-form note: build ids, a short reason, etc. NEVER secrets. */
23
+ note?: string;
24
+ }
25
+ export interface ShipState {
26
+ slug: string;
27
+ /** Schema version of this state file (forward-compatible migrations). */
28
+ stateVersion: 1;
29
+ /**
30
+ * The app marketing version the recorded phases shipped. When it changes, the
31
+ * orchestrator invalidates the binary-scoped phases (build → release) so a
32
+ * version bump rebuilds instead of skipping a stale `done`.
33
+ */
34
+ appVersion?: string;
35
+ phases: Partial<Record<Phase, PhaseRecord>>;
36
+ }
37
+ /** Path to the state file for a slug, under `<baseDir>/.amba/ship-state/`. */
38
+ export declare function shipStatePath(slug: string, baseDir?: string): string;
39
+ /** Load state for a slug, or a fresh empty state if none exists. Pure read. */
40
+ export declare function loadShipState(slug: string, baseDir?: string): ShipState;
41
+ /** Write state atomically (write temp, then rename) to avoid partial files. */
42
+ export declare function saveShipState(state: ShipState, baseDir?: string): void;
43
+ /**
44
+ * Return a NEW state with one phase updated. Pure: does not touch disk and does
45
+ * not read the clock — the caller passes `timestamp`. Use `saveShipState` to
46
+ * persist.
47
+ */
48
+ export declare function withPhase(state: ShipState, phase: Phase, status: PhaseStatus, timestamp: string, note?: string): ShipState;
49
+ /** True if a phase is recorded as done. Used by the orchestrator for idempotency. */
50
+ export declare function isPhaseDone(state: ShipState, phase: Phase): boolean;
51
+ /** The record for a phase, or a synthetic pending record if never run. */
52
+ export declare function phaseRecord(state: ShipState, phase: Phase): PhaseRecord;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.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.",
3
+ "version": "4.1.1",
4
+ "description": "amba — agent-native backend-as-a-service. Functions, collections, storage, AI, email, queues, sites, and `amba ship` to take an Expo app live on the App Store + Google Play. `npx @layers/amba init` to start.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "amba": "dist/index.js"
@@ -176,7 +176,7 @@ For each surface in the confirmed set, read the relevant reference file and exec
176
176
  - **gamification** (XP rules, achievements, streaks, leaderboards, challenges) → `references/gamification.md`
177
177
  - **economy** (currencies, catalog, stores, inventory) → `references/economy.md`
178
178
  - **social** (friends, groups, feeds, messaging, moderation, reviews) → `references/social.md`
179
- - **infrastructure** (collections / DB tables, functions, analytics, AI prompts) → `references/infrastructure.md`
179
+ - **infrastructure** (collections / relational Postgres tables, functions, analytics, AI prompts) → `references/infrastructure.md`
180
180
 
181
181
  Each reference file has, in order:
182
182
 
@@ -1,14 +1,14 @@
1
1
  # Infrastructure
2
2
 
3
- The plumbing that sits behind every other surface: custom database tables (Collections — schema-first, per-tenant), serverless functions (run server-side code without standing up a backend), analytics (events + sessions), AI prompts (managed LLM templates, callable from the SDK with per-tenant keys), secrets, runtime configs, feature flags, third-party integrations (RevenueCat / Superwall / Resend / Stripe / etc.), media (file storage + CDN), and sites (static asset hosting at `*.app.amba.host`).
3
+ The plumbing that sits behind every other surface: relational Postgres tables (Collections — schema-first, per-tenant), serverless functions (run server-side code without standing up a backend), analytics (events + sessions), AI prompts (managed LLM templates, callable from the SDK with per-tenant keys), secrets, runtime configs, feature flags, third-party integrations (RevenueCat / Superwall / Resend / Stripe / etc.), media (file storage + CDN), and sites (static asset hosting at `*.app.amba.host`).
4
4
 
5
5
  If gamification, economy, and social are the playable surface, **infrastructure is what you build a custom product on top of**. Anything that doesn't fit the canned surfaces lands here.
6
6
 
7
7
  ## MCP tools
8
8
 
9
- ### Collections (typed tables)
9
+ ### Collections (relational Postgres tables)
10
10
 
11
- A collection is a schema-first table inside the project's isolated tenant database. You describe the columns, the server creates the table and any indexes. Rows are scoped to the signed-in `app_user` automatically (server-enforced auto row-level isolation) for SDK clients — admin tools bypass this.
11
+ A collection is a relational Postgres table inside the project's isolated tenant database — typed columns, foreign keys, transactions, unique indexes, and vector search. You describe the columns, the server creates the table and any indexes. Rows are scoped to the signed-in `app_user` automatically (server-enforced auto row-level isolation) for SDK clients — admin tools bypass this.
12
12
 
13
13
  | Tool | Purpose | Example args |
14
14
  | --- | --- | --- |