@layers/amba 4.0.4 → 4.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 +3 -3
- package/dist/api-client.d.ts +4 -1
- package/dist/commands/collections.d.ts +55 -0
- package/dist/commands/functions-logs.d.ts +11 -0
- package/dist/commands/monetization.d.ts +14 -4
- package/dist/commands/ship.d.ts +47 -0
- package/dist/index.js +1391 -57
- package/dist/ship/config.d.ts +98 -0
- package/dist/ship/exec.d.ts +53 -0
- package/dist/ship/orchestrator.d.ts +52 -0
- package/dist/ship/phases.d.ts +87 -0
- package/dist/ship/state.d.ts +52 -0
- package/package.json +4 -4
|
@@ -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
|
-
"description": "amba — agent-native backend-as-a-service. Functions, collections, storage, AI, email, queues, sites
|
|
3
|
+
"version": "4.1.0",
|
|
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"
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"tsdown": "^0.12.5",
|
|
50
50
|
"typescript": "^5.8.3",
|
|
51
51
|
"vitest": "^3.2.4",
|
|
52
|
-
"@layers/amba-
|
|
53
|
-
"@layers/amba-
|
|
52
|
+
"@layers/amba-shared": "4.0.4",
|
|
53
|
+
"@layers/amba-mcp": "4.0.6"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsdown && tsc --emitDeclarationOnly",
|