@capuchoo/core 0.1.0 → 0.1.2
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/dist/index.d.ts +38 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -59,6 +59,17 @@ interface UpdateCheckRequest {
|
|
|
59
59
|
/** Stable per-install identifier, used for channel overrides and stats. */
|
|
60
60
|
deviceId?: string;
|
|
61
61
|
isProd?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Device facts the server stores but does not decide with. All optional: an
|
|
64
|
+
* app that cannot determine one should omit it rather than send a placeholder,
|
|
65
|
+
* because the server writes only the keys it receives and a placeholder would
|
|
66
|
+
* overwrite a better value recorded earlier.
|
|
67
|
+
*/
|
|
68
|
+
versionOs?: string;
|
|
69
|
+
pluginVersion?: string;
|
|
70
|
+
isEmulator?: boolean;
|
|
71
|
+
/** Caller-supplied label for this install, shown in the dashboard. */
|
|
72
|
+
customId?: string;
|
|
62
73
|
}
|
|
63
74
|
/** A native binary (APK/IPA) the device should install. */
|
|
64
75
|
interface NativeUpdatePayload {
|
|
@@ -133,6 +144,32 @@ interface UpdateEventPayload {
|
|
|
133
144
|
error?: string;
|
|
134
145
|
}
|
|
135
146
|
//#endregion
|
|
147
|
+
//#region src/channel-environment.d.ts
|
|
148
|
+
/**
|
|
149
|
+
* A channel's `environment` decides which `.env` flavour the CLI builds and which
|
|
150
|
+
* bundles the backend serves to it. The channel's *name* is only an identifier.
|
|
151
|
+
*
|
|
152
|
+
* Nothing links the two, so a channel named `prod` left on the `staging`
|
|
153
|
+
* environment silently serves staging bundles to production devices - which is
|
|
154
|
+
* how all three of Lowmaro's channels ended up on staging. These helpers make
|
|
155
|
+
* the mismatch visible; they never correct it silently, because prod apps
|
|
156
|
+
* legitimately point at a staging channel for beta testing.
|
|
157
|
+
*/
|
|
158
|
+
/** Empty means "not chosen yet". A channel must not default into an environment. */
|
|
159
|
+
type EnvironmentSelection = Environment | "";
|
|
160
|
+
/**
|
|
161
|
+
* The environment a channel name implies, or `null` when the name says nothing.
|
|
162
|
+
*
|
|
163
|
+
* Matching is deliberately whole-name: a channel called `prod-eu` could belong
|
|
164
|
+
* to either, and guessing at substrings would put a warning on names it cannot
|
|
165
|
+
* reason about.
|
|
166
|
+
*/
|
|
167
|
+
declare function suggestEnvironment(name: string): Environment | null;
|
|
168
|
+
/** True when the name implies one environment and a different one is selected. */
|
|
169
|
+
declare function hasEnvironmentMismatch(name: string, environment: EnvironmentSelection): boolean;
|
|
170
|
+
/** The warning to show for a mismatch, or `null` when there is nothing to warn about. */
|
|
171
|
+
declare function environmentMismatchWarning(name: string, environment: EnvironmentSelection): string | null;
|
|
172
|
+
//#endregion
|
|
136
173
|
//#region src/project-config.d.ts
|
|
137
174
|
/**
|
|
138
175
|
* `.capucho/project.json` - the file that makes an application deployable.
|
|
@@ -367,5 +404,5 @@ interface UserProfile {
|
|
|
367
404
|
declare const APP_CREATOR_ROLES: ReadonlySet<string>;
|
|
368
405
|
declare function canCreateApps(organization: CloudOrganization): boolean;
|
|
369
406
|
//#endregion
|
|
370
|
-
export { APP_CREATOR_ROLES, type BuildConfig, type BumpType, type CloudApp, type CloudChannel, type CloudOrganization, type CloudRelease, type CloudUser, ENVIRONMENTS, type Environment, type FlavourConfig, INITIAL_VERSION_CODES, type NativeUpdatePayload, PROJECT_CONFIG_VERSION, type Platform, type ProjectConfig, type ResolvedProjectConfig, type ResolvedUpdate, type SemanticVersion, type UpdateCheckRequest, type UpdateCheckResponse, type UpdateEvent, type UpdateEventPayload, type UpdateKind, UpdateMessage, type UpdateMessageValue, type UserProfile, type VersionCodes, bumpVersion, canCreateApps, compareVersions, defaultFlavour, describeEnvironmentMismatch, environmentFromAppId, formatVersion, isBlockingResponse, isEnvironmentAllowed, isValidBundleId, nextVersionCode, normaliseProjectConfig, parseVersion, resolveUpdate, validateProjectConfig, versionEnv };
|
|
407
|
+
export { APP_CREATOR_ROLES, type BuildConfig, type BumpType, type CloudApp, type CloudChannel, type CloudOrganization, type CloudRelease, type CloudUser, ENVIRONMENTS, type Environment, type EnvironmentSelection, type FlavourConfig, INITIAL_VERSION_CODES, type NativeUpdatePayload, PROJECT_CONFIG_VERSION, type Platform, type ProjectConfig, type ResolvedProjectConfig, type ResolvedUpdate, type SemanticVersion, type UpdateCheckRequest, type UpdateCheckResponse, type UpdateEvent, type UpdateEventPayload, type UpdateKind, UpdateMessage, type UpdateMessageValue, type UserProfile, type VersionCodes, bumpVersion, canCreateApps, compareVersions, defaultFlavour, describeEnvironmentMismatch, environmentFromAppId, environmentMismatchWarning, formatVersion, hasEnvironmentMismatch, isBlockingResponse, isEnvironmentAllowed, isValidBundleId, nextVersionCode, normaliseProjectConfig, parseVersion, resolveUpdate, suggestEnvironment, validateProjectConfig, versionEnv };
|
|
371
408
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/update-contract.ts","../src/project-config.ts","../src/version.ts","../src/cloud.ts"],"mappings":";;;;;;;;;;;;KAYY;;KAGA;;;;;cAMC;;;;;;;;;;;;;;;KAgBD,6BAA6B,4BAA4B;;UAGpD;;EAEf;EACA,UAAU;;EAEV;EACA;;;;;;;EAOA;;EAEA;;;;;;EAMA;;EAEA;EACA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;;;;;;;UAQe;EACf;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;;EAGA,gBAAgB;;EAGhB,SAAS;;KAGC;;UAGK;EACf,MAAM;EACN;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;EACA;;EAEA;;;;;;;;;;iBAWc,cACd,UAAU,yCACT;;;;;iBAsCa,mBAAmB,UAAU;;KAQjC;UAQK;EACf,OAAO;EACP,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/update-contract.ts","../src/channel-environment.ts","../src/project-config.ts","../src/version.ts","../src/cloud.ts"],"mappings":";;;;;;;;;;;;KAYY;;KAGA;;;;;cAMC;;;;;;;;;;;;;;;KAgBD,6BAA6B,4BAA4B;;UAGpD;;EAEf;EACA,UAAU;;EAEV;EACA;;;;;;;EAOA;;EAEA;;;;;;EAMA;;EAEA;EACA;;;;;;;EAOA;EACA;EACA;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;;;;;;;UAQe;EACf;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;;EAGA,gBAAgB;;EAGhB,SAAS;;KAGC;;UAGK;EACf,MAAM;EACN;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;EACA;;EAEA;;;;;;;;;;iBAWc,cACd,UAAU,yCACT;;;;;iBAsCa,mBAAmB,UAAU;;KAQjC;UAQK;EACf,OAAO;EACP,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;;;;KC7LU,uBAAuB;;;;;;;;iBAenB,mBAAmB,eAAe;;iBAYlC,uBAAuB,cAAc,aAAa;;iBAQlD,2BACd,cACA,aAAa;;;;;;;;;;;;;;;;;cCnCF;;UAGI;;;;;;;EAOf;;EAEA;;EAEA;;EAEA;;UAGe;;;;;EAKf;;EAEA;;UAGe;;EAEf;;EAGA;;EAEA;EACA;EACA;;EAGA;EACA;EACA;;;;;EAMA;EAEA,WAAW,QAAQ,OAAO,aAAa;EACvC,QAAQ;;EAGR;;EAGA;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,UAAU,OAAO,aAAa;EAC9B,OAAO;EACP;;cAGW,uBAAuB;;;;;iBAMpB,eAAe,aAAa,cAAc;iBAS1C,uBAAuB,QAAQ,gBAAgB;;iBAuC/C,sBAAsB,QAAQ,QAAQ;iBAiBtC,gBAAgB;;;;;;;;iBAWhB,qBAAqB,gBAAgB;;;;;;;;;;;;;;iBAoBrC,qBAAqB,eAAe,oBAAoB;;iBAOxD,4BACd,eACA,oBAAoB,aACpB;;;;;;;;;;;;;KClMU;UAEK;EACf;EACA;EACA;EACA;EACA;;iBAMc,aAAa,gBAAgB;iBAa7B,cAAc,SAAS;;;;;iBAWvB,YAAY,eAAe,MAAM;;;;;;;iBA8BjC,gBAAgB,WAAW;KAsB/B,eAAe,OAAO;cAErB,uBAAuB;;;;;;;iBAYpB,gBACd,OAAO,QAAQ,kCACf,aAAa,cACZ;;;;;;;;;;;iBAea,WAAW,iBAAiB;;;;;;;;UClI3B;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;;;;;EAKA,aAAa;EACb;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;;;UAIe;EACf,MAAM;EACN,eAAe;EACf,MAAM,MAAM;IAAa;;;;cAId,mBAAmB;iBAEhB,cAAc,cAAc"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
//#region src/channel-environment.ts
|
|
2
|
+
const PATTERNS = [
|
|
3
|
+
["prod", /^(prod|production|live|release|stable|main|master)$/],
|
|
4
|
+
["staging", /^(staging|stage|beta|uat|qa|test|preprod|pre-prod)$/],
|
|
5
|
+
["dev", /^(dev|develop|development|debug|local|alpha)$/]
|
|
6
|
+
];
|
|
7
|
+
/**
|
|
8
|
+
* The environment a channel name implies, or `null` when the name says nothing.
|
|
9
|
+
*
|
|
10
|
+
* Matching is deliberately whole-name: a channel called `prod-eu` could belong
|
|
11
|
+
* to either, and guessing at substrings would put a warning on names it cannot
|
|
12
|
+
* reason about.
|
|
13
|
+
*/
|
|
14
|
+
function suggestEnvironment(name) {
|
|
15
|
+
const normalized = name.trim().toLowerCase();
|
|
16
|
+
if (!normalized) return null;
|
|
17
|
+
for (const [environment, pattern] of PATTERNS) if (pattern.test(normalized)) return environment;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/** True when the name implies one environment and a different one is selected. */
|
|
21
|
+
function hasEnvironmentMismatch(name, environment) {
|
|
22
|
+
if (!environment) return false;
|
|
23
|
+
const suggested = suggestEnvironment(name);
|
|
24
|
+
return suggested !== null && suggested !== environment;
|
|
25
|
+
}
|
|
26
|
+
/** The warning to show for a mismatch, or `null` when there is nothing to warn about. */
|
|
27
|
+
function environmentMismatchWarning(name, environment) {
|
|
28
|
+
if (!hasEnvironmentMismatch(name, environment)) return null;
|
|
29
|
+
const label = name.trim();
|
|
30
|
+
return `A channel named "${label}" is set to the ${environment} environment. Devices on it will receive ${environment} bundles, built from .env.${environment}. Set the environment to ${suggestEnvironment(label)} unless that is deliberate.`;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
1
33
|
//#region src/update-contract.ts
|
|
2
34
|
/**
|
|
3
35
|
* Messages the backend puts in `message`. Both sides must agree on the exact
|
|
@@ -281,6 +313,6 @@ function canCreateApps(organization) {
|
|
|
281
313
|
return APP_CREATOR_ROLES.has(organization.role);
|
|
282
314
|
}
|
|
283
315
|
//#endregion
|
|
284
|
-
export { APP_CREATOR_ROLES, ENVIRONMENTS, INITIAL_VERSION_CODES, PROJECT_CONFIG_VERSION, UpdateMessage, bumpVersion, canCreateApps, compareVersions, defaultFlavour, describeEnvironmentMismatch, environmentFromAppId, formatVersion, isBlockingResponse, isEnvironmentAllowed, isValidBundleId, nextVersionCode, normaliseProjectConfig, parseVersion, resolveUpdate, validateProjectConfig, versionEnv };
|
|
316
|
+
export { APP_CREATOR_ROLES, ENVIRONMENTS, INITIAL_VERSION_CODES, PROJECT_CONFIG_VERSION, UpdateMessage, bumpVersion, canCreateApps, compareVersions, defaultFlavour, describeEnvironmentMismatch, environmentFromAppId, environmentMismatchWarning, formatVersion, hasEnvironmentMismatch, isBlockingResponse, isEnvironmentAllowed, isValidBundleId, nextVersionCode, normaliseProjectConfig, parseVersion, resolveUpdate, suggestEnvironment, validateProjectConfig, versionEnv };
|
|
285
317
|
|
|
286
318
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/update-contract.ts","../src/project-config.ts","../src/version.ts","../src/cloud.ts"],"sourcesContent":["/**\n * The wire contract for `POST {endpoint}/api/update`.\n *\n * This file is the single definition shared by the backend that produces the\n * response, the app runtime that consumes it, and the CLI that publishes the\n * artefacts it points at. Before this package existed the three had drifted:\n * the app template asked a second endpoint (`GET /api/native-updates/check`)\n * with an `{ available, update }` envelope the backend never returns on the\n * primary path, and sent a hard-coded `version_name: \"builtin\"` so the server\n * always compared against 0.0.0.\n */\n\nexport type Platform = \"android\" | \"ios\" | \"web\";\n\n/** Deployment environments. A channel is bound to exactly one of these. */\nexport type Environment = \"dev\" | \"staging\" | \"prod\";\n\n/**\n * Messages the backend puts in `message`. Both sides must agree on the exact\n * strings, so they live here rather than being retyped at each call site.\n */\nexport const UpdateMessage = {\n /** A newer native binary is assigned to the channel and must be installed. */\n NATIVE_UPDATE_REQUIRED: \"native_update_required\",\n /** A newer artefact is available. */\n UPDATE_AVAILABLE: \"update_available\",\n /** The device already runs the newest artefact for its channel. */\n NO_UPDATE: \"No update available\",\n /** The channel name does not exist for this application. */\n CHANNEL_NOT_FOUND: \"Channel not found\",\n /**\n * The requesting app id does not belong to the channel's environment - a\n * staging build asking a production channel, for example.\n */\n ENVIRONMENT_MISMATCH: \"Environment mismatch\",\n} as const;\n\nexport type UpdateMessageValue = (typeof UpdateMessage)[keyof typeof UpdateMessage];\n\n/** What the device tells the server about itself. */\nexport interface UpdateCheckRequest {\n /** Bundle identifier of the running build, e.g. `com.ayb.lowmaro.staging`. */\n appId: string;\n platform: Platform;\n /** Channel to consult. Falls back to `defaultChannel` server-side. */\n channel?: string;\n defaultChannel?: string;\n /**\n * Native build number as a string. The server compares this against\n * `native_updates.version_code` and against an OTA bundle's\n * `min_update_version`, so an omitted or wrong value silently disables\n * native-update gating.\n */\n versionCode?: string;\n /** Historical alias for `versionCode`; the server accepts either. */\n versionBuild?: string;\n /**\n * Semantic version of the *currently applied web bundle*, or `\"builtin\"`\n * when the app still runs the bundle shipped inside the binary. Sending a\n * constant here defeats version comparison entirely.\n */\n version_name?: string;\n /** Stable per-install identifier, used for channel overrides and stats. */\n deviceId?: string;\n isProd?: boolean;\n}\n\n/** A native binary (APK/IPA) the device should install. */\nexport interface NativeUpdatePayload {\n version_name: string;\n version_code: number;\n download_url: string;\n release_notes?: string;\n required?: boolean;\n platform?: Platform;\n file_size?: number;\n}\n\n/**\n * The response. Every field is optional because the server returns a partial\n * object per outcome rather than a discriminated union - `resolveUpdate` below\n * narrows it into something a caller can branch on safely.\n */\nexport interface UpdateCheckResponse {\n message?: string;\n error?: string;\n\n /** OTA bundle fields. */\n version_name?: string;\n url?: string;\n checksum?: string;\n sessionKey?: string;\n release_notes?: string;\n required?: boolean;\n\n /** Present when a native binary supersedes, or blocks, the OTA bundle. */\n native_update?: NativeUpdatePayload | null;\n\n /** Remote configuration resolved for the channel's environment. */\n config?: Record<string, string>;\n}\n\nexport type UpdateKind = \"native\" | \"ota\";\n\n/** A resolved, actionable update. */\nexport interface ResolvedUpdate {\n kind: UpdateKind;\n version: string;\n versionCode?: number;\n downloadUrl?: string;\n releaseNotes?: string;\n required: boolean;\n platform?: Platform;\n checksum?: string;\n sessionKey?: string;\n /** Set once the OTA plugin has downloaded the bundle. */\n bundleId?: string;\n}\n\n/**\n * Narrows a raw response into an update to act on, or `null` when there is\n * nothing to do.\n *\n * Native wins over OTA. The server can return both - a required native binary\n * alongside the OTA bundle that needs it - and installing the bundle first\n * would leave the device on a binary too old to run it.\n */\nexport function resolveUpdate(\n response: UpdateCheckResponse | null | undefined,\n): ResolvedUpdate | null {\n if (!response) return null;\n\n const native = response.native_update;\n if (native?.download_url) {\n return {\n kind: \"native\",\n version: native.version_name,\n versionCode: native.version_code,\n downloadUrl: native.download_url,\n releaseNotes: native.release_notes,\n // A native update is mandatory when the server says the OTA bundle\n // cannot run without it, whatever the record's own flag says.\n required:\n response.message === UpdateMessage.NATIVE_UPDATE_REQUIRED || (native.required ?? false),\n platform: native.platform,\n };\n }\n\n if (response.url && response.version_name) {\n return {\n kind: \"ota\",\n version: response.version_name,\n downloadUrl: response.url,\n releaseNotes: response.release_notes,\n required: response.required ?? false,\n checksum: response.checksum,\n sessionKey: response.sessionKey,\n };\n }\n\n return null;\n}\n\n/**\n * True when the response reports a condition the user cannot fix by updating.\n * Callers should surface these instead of showing \"you are up to date\".\n */\nexport function isBlockingResponse(response: UpdateCheckResponse): boolean {\n return (\n response.message === UpdateMessage.CHANNEL_NOT_FOUND ||\n response.message === UpdateMessage.ENVIRONMENT_MISMATCH\n );\n}\n\n/** Analytics events posted to `POST {endpoint}/api/native-updates/log`. */\nexport type UpdateEvent =\n | \"check\"\n | \"download\"\n | \"download_complete\"\n | \"install\"\n | \"cancel\"\n | \"error\";\n\nexport interface UpdateEventPayload {\n event: UpdateEvent;\n platform: Platform;\n device_id: string;\n current_version_code: number;\n new_version?: string;\n new_version_code?: number;\n channel: string;\n environment: string;\n error?: string;\n}\n","import type { Environment } from \"./update-contract.js\";\n\n/**\n * `.capucho/project.json` - the file that makes an application deployable.\n *\n * Version 1 held only the cloud identifiers, so the CLI had to *guess* how to\n * build: it shelled out to `pnpm run assets:<env>`, `pnpm build:<env>`,\n * `pnpm trapeze:<env>` and `pnpm exec cap sync`. Any application that named\n * its scripts differently, used npm, or had not installed Trapeze simply\n * failed halfway through a deploy.\n *\n * Version 2 describes the *inputs* instead - where each flavour's env file,\n * Trapeze config and icon sources live - and lets the CLI own the execution.\n * Every field has a default, so a v1 file keeps working: `normaliseProjectConfig`\n * fills in the conventional layout both existing apps already use.\n */\nexport const PROJECT_CONFIG_VERSION = 2;\n\n/** One build flavour, keyed by the environment its channels are bound to. */\nexport interface FlavourConfig {\n /**\n * Env file supplying `VITE_APP_ID`, `VITE_APP_NAME`, `VITE_UPDATE_CHANNEL`\n * and friends. The CLI reads it and passes the values to the build and to\n * the native configuration step as environment variables - it does not\n * rewrite the file, so a deploy leaves the working tree clean.\n */\n envFile: string;\n /** Trapeze config applied to the native projects. Optional. */\n trapezeConfig?: string;\n /** Directory holding `icon.png` / `splash.png` for icon generation. */\n assetPath?: string;\n /** Vite `--mode`. Defaults to the flavour name. */\n mode?: string;\n}\n\nexport interface BuildConfig {\n /**\n * Overrides the web build. Leave unset and the CLI runs the project's own\n * Vite build through the workspace toolchain it detects.\n */\n command?: string;\n /** Where to run the build from, relative to the app. For monorepo roots. */\n cwd?: string;\n}\n\nexport interface ProjectConfig {\n /** Absent on v1 files. */\n version?: number;\n\n /** Bundle identifier of the production flavour. */\n appId: string;\n /** Primary key of the application in Capucho. */\n cloudAppId: string;\n appName: string;\n createdAt: string;\n\n /** Vite output directory, and what Capacitor copies into the native app. */\n webDir?: string;\n androidDir?: string;\n iosDir?: string;\n\n /**\n * Monotonic native build numbers per environment. Written by the CLI, and\n * the one file a deploy is expected to modify.\n */\n versionCodeFile?: string;\n\n flavours?: Partial<Record<Environment, FlavourConfig>>;\n build?: BuildConfig;\n\n /** Optional GitHub Pages mirror for generated web assets. */\n ghPagesRepo?: string;\n\n /** @deprecated v1 fields, folded into `build` by `normaliseProjectConfig`. */\n monorepoRoot?: string;\n /** @deprecated v1 field. */\n packageName?: string;\n}\n\n/** A `ProjectConfig` with every optional resolved. */\nexport interface ResolvedProjectConfig {\n version: number;\n appId: string;\n cloudAppId: string;\n appName: string;\n createdAt: string;\n webDir: string;\n androidDir: string;\n iosDir: string;\n versionCodeFile: string;\n flavours: Record<Environment, FlavourConfig>;\n build: BuildConfig;\n ghPagesRepo?: string;\n}\n\nexport const ENVIRONMENTS: readonly Environment[] = [\"dev\", \"staging\", \"prod\"];\n\n/**\n * The layout both existing applications already use. Used as the default so a\n * v1 `project.json` needs no migration to keep deploying.\n */\nexport function defaultFlavour(environment: Environment): FlavourConfig {\n return {\n envFile: `build/${environment}/.env.${environment}`,\n trapezeConfig: `build/${environment}/trapeze.${environment}.yaml`,\n assetPath: `build/${environment}/assets`,\n mode: environment,\n };\n}\n\nexport function normaliseProjectConfig(config: ProjectConfig): ResolvedProjectConfig {\n const flavours = {} as Record<Environment, FlavourConfig>;\n for (const environment of ENVIRONMENTS) {\n const defaults = defaultFlavour(environment);\n const declared = config.flavours?.[environment];\n flavours[environment] = {\n envFile: declared?.envFile ?? defaults.envFile,\n trapezeConfig: declared?.trapezeConfig ?? defaults.trapezeConfig,\n assetPath: declared?.assetPath ?? defaults.assetPath,\n mode: declared?.mode ?? defaults.mode,\n };\n }\n\n // v1 expressed monorepo builds as `monorepoRoot` + `packageName`, which the\n // CLI turned into `pnpm exec vp run <pkg>#build:<env>`. Carry that forward as\n // an explicit build command so the behaviour is visible rather than implied.\n const build: BuildConfig = { ...config.build };\n if (!build.cwd && config.monorepoRoot) build.cwd = config.monorepoRoot;\n if (!build.command && config.packageName) {\n build.command = `vp run ${config.packageName}#build`;\n }\n\n return {\n version: config.version ?? 1,\n appId: config.appId,\n cloudAppId: config.cloudAppId,\n appName: config.appName,\n createdAt: config.createdAt,\n webDir: config.webDir ?? \"dist\",\n androidDir: config.androidDir ?? \"android\",\n iosDir: config.iosDir ?? \"ios\",\n versionCodeFile: config.versionCodeFile ?? \"version-code.json\",\n flavours,\n build,\n ghPagesRepo: config.ghPagesRepo,\n };\n}\n\n/** Fields a `project.json` must carry for a deploy to be possible. */\nexport function validateProjectConfig(config: Partial<ProjectConfig> | null | undefined): string[] {\n if (!config) return [\"project.json is missing or empty\"];\n\n const problems: string[] = [];\n if (!config.appId) problems.push(\"appId is required\");\n if (!config.cloudAppId) problems.push(\"cloudAppId is required\");\n if (!config.appName) problems.push(\"appName is required\");\n\n if (config.appId && !isValidBundleId(config.appId)) {\n problems.push(`appId \"${config.appId}\" is not a valid bundle identifier`);\n }\n\n return problems;\n}\n\nconst BUNDLE_ID = /^[a-z][a-z\\d_]*(\\.[a-z][a-z\\d_]*)+$/;\n\nexport function isValidBundleId(value: string): boolean {\n return BUNDLE_ID.test(value);\n}\n\n/**\n * Derives the environment a bundle identifier belongs to.\n *\n * The backend enforces the same rule server-side: a `.staging` build may only\n * be served staging channels. Mirroring it here lets the CLI refuse a\n * mismatched deploy before it uploads several megabytes.\n */\nexport function environmentFromAppId(appId: string): Environment {\n const id = appId.toLowerCase();\n if (id.endsWith(\".staging\")) return \"staging\";\n if (id.endsWith(\".dev\") || id.endsWith(\".debug\")) return \"dev\";\n return \"prod\";\n}\n\n/**\n * Whether a build may be served a channel bound to `channelEnvironment`.\n *\n * This mirrors the server's isolation check exactly, including its one\n * deliberate exception: a production build is allowed on a staging channel, so\n * a release candidate can be beta-tested by real installs without shipping a\n * separate bundle identifier.\n *\n * The rule lives here rather than being restated at each call site because the\n * CLI had reimplemented it as a plain equality check - which is *stricter* than\n * the server and rejected the exact beta-testing setup Lowmaro uses, where all\n * three channels are bound to staging and the app id carries no suffix.\n */\nexport function isEnvironmentAllowed(appId: string, channelEnvironment: Environment): boolean {\n const expected = environmentFromAppId(appId);\n if (expected === channelEnvironment) return true;\n return expected === \"prod\" && channelEnvironment === \"staging\";\n}\n\n/** Explains a rejected pairing, or null when it is allowed. */\nexport function describeEnvironmentMismatch(\n appId: string,\n channelEnvironment: Environment,\n channelName: string,\n): string | null {\n if (isEnvironmentAllowed(appId, channelEnvironment)) return null;\n\n const expected = environmentFromAppId(appId);\n return (\n `Channel \"${channelName}\" serves the ${channelEnvironment} environment, but ` +\n `the build's VITE_APP_ID is \"${appId}\", which is a ${expected} bundle id. ` +\n \"The server rejects this pairing, so the upload would be wasted.\"\n );\n}\n","import type { Environment } from \"./update-contract.js\";\n\n/**\n * Version arithmetic, kept free of any filesystem or child-process access so\n * both the CLI and the tests can use it directly.\n *\n * The CLI used to shell out to `npm version <type> --no-git-tag-version` for\n * this. In a workspace that is actively wrong: npm resolves the *nearest*\n * package.json, so running it from a monorepo root bumped the root package\n * instead of the app, and it mixed npm into a pnpm/Vite+ project for a job\n * that is three lines of string handling.\n */\n\nexport type BumpType = \"major\" | \"minor\" | \"patch\";\n\nexport interface SemanticVersion {\n major: number;\n minor: number;\n patch: number;\n prerelease?: string;\n build?: string;\n}\n\nconst SEMVER =\n /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([\\dA-Za-z-]+(?:\\.[\\dA-Za-z-]+)*))?(?:\\+([\\dA-Za-z-]+(?:\\.[\\dA-Za-z-]+)*))?$/;\n\nexport function parseVersion(value: string): SemanticVersion | null {\n const match = SEMVER.exec(value.trim());\n if (!match) return null;\n\n return {\n major: Number(match[1]),\n minor: Number(match[2]),\n patch: Number(match[3]),\n prerelease: match[4],\n build: match[5],\n };\n}\n\nexport function formatVersion(version: SemanticVersion): string {\n let out = `${version.major}.${version.minor}.${version.patch}`;\n if (version.prerelease) out += `-${version.prerelease}`;\n if (version.build) out += `+${version.build}`;\n return out;\n}\n\n/**\n * Bumps a version string. Prerelease and build metadata are dropped, matching\n * `npm version` semantics for a plain major/minor/patch bump.\n */\nexport function bumpVersion(value: string, type: BumpType): string {\n const parsed = parseVersion(value);\n if (!parsed) {\n throw new Error(`\"${value}\" is not a semantic version, so it cannot be bumped`);\n }\n\n switch (type) {\n case \"major\":\n return formatVersion({ major: parsed.major + 1, minor: 0, patch: 0 });\n case \"minor\":\n return formatVersion({\n major: parsed.major,\n minor: parsed.minor + 1,\n patch: 0,\n });\n case \"patch\":\n return formatVersion({\n major: parsed.major,\n minor: parsed.minor,\n patch: parsed.patch + 1,\n });\n }\n}\n\n/**\n * Compares two semantic versions. Returns a negative number when `a` is older.\n *\n * A missing or unparseable version sorts oldest, which is what the app needs:\n * the sentinel `\"builtin\"` must always look older than any published bundle.\n */\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n\n if (!left && !right) return 0;\n if (!left) return -1;\n if (!right) return 1;\n\n if (left.major !== right.major) return left.major - right.major;\n if (left.minor !== right.minor) return left.minor - right.minor;\n if (left.patch !== right.patch) return left.patch - right.patch;\n\n // 1.0.0-beta precedes 1.0.0.\n if (left.prerelease && !right.prerelease) return -1;\n if (!left.prerelease && right.prerelease) return 1;\n if (left.prerelease && right.prerelease) {\n return left.prerelease < right.prerelease ? -1 : left.prerelease > right.prerelease ? 1 : 0;\n }\n\n return 0;\n}\n\nexport type VersionCodes = Record<Environment, number>;\n\nexport const INITIAL_VERSION_CODES: VersionCodes = {\n dev: 1,\n staging: 1,\n prod: 1,\n};\n\n/**\n * Native build numbers must increase monotonically per environment: Android\n * refuses to install an APK whose versionCode is not greater than the\n * installed one, and the backend uses the same number to decide whether a\n * native update supersedes an OTA bundle.\n */\nexport function nextVersionCode(\n codes: Partial<VersionCodes> | null | undefined,\n environment: Environment,\n): VersionCodes {\n const current: VersionCodes = { ...INITIAL_VERSION_CODES, ...codes };\n return { ...current, [environment]: (current[environment] ?? 0) + 1 };\n}\n\n/**\n * Build-time variables injected into the web build and the native\n * configuration step.\n *\n * These used to be written *into* the committed `build/<env>/.env.<env>` file\n * by every deploy, which dirtied the working tree and made two concurrent\n * deploys race over one file. They are environment variables now: Trapeze\n * reads its `vars:` block from the process environment, and Vite reads\n * `VITE_*` the same way.\n */\nexport function versionEnv(version: string, versionCode: number) {\n return {\n VITE_APP_VERSION: version,\n VERSION_CODE: String(versionCode),\n BUILD_NUMBER: String(versionCode),\n };\n}\n","import type { Environment, Platform } from \"./update-contract.js\";\n\n/** Shapes returned by the authenticated `/api/*` endpoints. */\n\nexport interface CloudOrganization {\n id: string;\n name: string;\n slug: string;\n role: \"owner\" | \"admin\" | \"member\";\n}\n\nexport interface CloudApp {\n id: string;\n name: string;\n app_id: string;\n platform: string;\n organization_id: string;\n created_at: string;\n icon_url?: string;\n}\n\nexport interface CloudChannel {\n id: string;\n name: string;\n app_id: string;\n /**\n * Which build flavour this channel serves. The CLI derives the whole build\n * from it, so a channel without an environment cannot be deployed to.\n */\n environment: Environment;\n public: boolean;\n created_at: string;\n current_version_id?: string | null;\n current_native_version_id?: string | null;\n}\n\nexport interface CloudRelease {\n id: string;\n version_name: string;\n platform: Platform;\n channel?: string;\n active: boolean;\n required: boolean;\n release_notes?: string;\n created_at: string;\n}\n\nexport interface CloudUser {\n id: string;\n email: string;\n role?: string;\n}\n\n/** Response of `GET /api/auth/me`. */\nexport interface UserProfile {\n user: CloudUser;\n organizations: CloudOrganization[];\n apps: Array<CloudApp & { role: string }>;\n}\n\n/** Roles allowed to create an application inside an organization. */\nexport const APP_CREATOR_ROLES: ReadonlySet<string> = new Set([\"owner\", \"admin\"]);\n\nexport function canCreateApps(organization: CloudOrganization): boolean {\n return APP_CREATOR_ROLES.has(organization.role);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/channel-environment.ts","../src/update-contract.ts","../src/project-config.ts","../src/version.ts","../src/cloud.ts"],"sourcesContent":["import type { Environment } from \"./update-contract.js\";\n\n/**\n * A channel's `environment` decides which `.env` flavour the CLI builds and which\n * bundles the backend serves to it. The channel's *name* is only an identifier.\n *\n * Nothing links the two, so a channel named `prod` left on the `staging`\n * environment silently serves staging bundles to production devices - which is\n * how all three of Lowmaro's channels ended up on staging. These helpers make\n * the mismatch visible; they never correct it silently, because prod apps\n * legitimately point at a staging channel for beta testing.\n */\n\n/** Empty means \"not chosen yet\". A channel must not default into an environment. */\nexport type EnvironmentSelection = Environment | \"\";\n\nconst PATTERNS: Array<[Environment, RegExp]> = [\n [\"prod\", /^(prod|production|live|release|stable|main|master)$/],\n [\"staging\", /^(staging|stage|beta|uat|qa|test|preprod|pre-prod)$/],\n [\"dev\", /^(dev|develop|development|debug|local|alpha)$/],\n];\n\n/**\n * The environment a channel name implies, or `null` when the name says nothing.\n *\n * Matching is deliberately whole-name: a channel called `prod-eu` could belong\n * to either, and guessing at substrings would put a warning on names it cannot\n * reason about.\n */\nexport function suggestEnvironment(name: string): Environment | null {\n const normalized = name.trim().toLowerCase();\n if (!normalized) return null;\n\n for (const [environment, pattern] of PATTERNS) {\n if (pattern.test(normalized)) return environment;\n }\n\n return null;\n}\n\n/** True when the name implies one environment and a different one is selected. */\nexport function hasEnvironmentMismatch(name: string, environment: EnvironmentSelection): boolean {\n if (!environment) return false;\n\n const suggested = suggestEnvironment(name);\n return suggested !== null && suggested !== environment;\n}\n\n/** The warning to show for a mismatch, or `null` when there is nothing to warn about. */\nexport function environmentMismatchWarning(\n name: string,\n environment: EnvironmentSelection,\n): string | null {\n if (!hasEnvironmentMismatch(name, environment)) return null;\n\n const label = name.trim();\n return (\n `A channel named \"${label}\" is set to the ${environment} environment. ` +\n `Devices on it will receive ${environment} bundles, built from .env.${environment}. ` +\n `Set the environment to ${suggestEnvironment(label)} unless that is deliberate.`\n );\n}\n","/**\n * The wire contract for `POST {endpoint}/api/update`.\n *\n * This file is the single definition shared by the backend that produces the\n * response, the app runtime that consumes it, and the CLI that publishes the\n * artefacts it points at. Before this package existed the three had drifted:\n * the app template asked a second endpoint (`GET /api/native-updates/check`)\n * with an `{ available, update }` envelope the backend never returns on the\n * primary path, and sent a hard-coded `version_name: \"builtin\"` so the server\n * always compared against 0.0.0.\n */\n\nexport type Platform = \"android\" | \"ios\" | \"web\";\n\n/** Deployment environments. A channel is bound to exactly one of these. */\nexport type Environment = \"dev\" | \"staging\" | \"prod\";\n\n/**\n * Messages the backend puts in `message`. Both sides must agree on the exact\n * strings, so they live here rather than being retyped at each call site.\n */\nexport const UpdateMessage = {\n /** A newer native binary is assigned to the channel and must be installed. */\n NATIVE_UPDATE_REQUIRED: \"native_update_required\",\n /** A newer artefact is available. */\n UPDATE_AVAILABLE: \"update_available\",\n /** The device already runs the newest artefact for its channel. */\n NO_UPDATE: \"No update available\",\n /** The channel name does not exist for this application. */\n CHANNEL_NOT_FOUND: \"Channel not found\",\n /**\n * The requesting app id does not belong to the channel's environment - a\n * staging build asking a production channel, for example.\n */\n ENVIRONMENT_MISMATCH: \"Environment mismatch\",\n} as const;\n\nexport type UpdateMessageValue = (typeof UpdateMessage)[keyof typeof UpdateMessage];\n\n/** What the device tells the server about itself. */\nexport interface UpdateCheckRequest {\n /** Bundle identifier of the running build, e.g. `com.ayb.lowmaro.staging`. */\n appId: string;\n platform: Platform;\n /** Channel to consult. Falls back to `defaultChannel` server-side. */\n channel?: string;\n defaultChannel?: string;\n /**\n * Native build number as a string. The server compares this against\n * `native_updates.version_code` and against an OTA bundle's\n * `min_update_version`, so an omitted or wrong value silently disables\n * native-update gating.\n */\n versionCode?: string;\n /** Historical alias for `versionCode`; the server accepts either. */\n versionBuild?: string;\n /**\n * Semantic version of the *currently applied web bundle*, or `\"builtin\"`\n * when the app still runs the bundle shipped inside the binary. Sending a\n * constant here defeats version comparison entirely.\n */\n version_name?: string;\n /** Stable per-install identifier, used for channel overrides and stats. */\n deviceId?: string;\n isProd?: boolean;\n /**\n * Device facts the server stores but does not decide with. All optional: an\n * app that cannot determine one should omit it rather than send a placeholder,\n * because the server writes only the keys it receives and a placeholder would\n * overwrite a better value recorded earlier.\n */\n versionOs?: string;\n pluginVersion?: string;\n isEmulator?: boolean;\n /** Caller-supplied label for this install, shown in the dashboard. */\n customId?: string;\n}\n\n/** A native binary (APK/IPA) the device should install. */\nexport interface NativeUpdatePayload {\n version_name: string;\n version_code: number;\n download_url: string;\n release_notes?: string;\n required?: boolean;\n platform?: Platform;\n file_size?: number;\n}\n\n/**\n * The response. Every field is optional because the server returns a partial\n * object per outcome rather than a discriminated union - `resolveUpdate` below\n * narrows it into something a caller can branch on safely.\n */\nexport interface UpdateCheckResponse {\n message?: string;\n error?: string;\n\n /** OTA bundle fields. */\n version_name?: string;\n url?: string;\n checksum?: string;\n sessionKey?: string;\n release_notes?: string;\n required?: boolean;\n\n /** Present when a native binary supersedes, or blocks, the OTA bundle. */\n native_update?: NativeUpdatePayload | null;\n\n /** Remote configuration resolved for the channel's environment. */\n config?: Record<string, string>;\n}\n\nexport type UpdateKind = \"native\" | \"ota\";\n\n/** A resolved, actionable update. */\nexport interface ResolvedUpdate {\n kind: UpdateKind;\n version: string;\n versionCode?: number;\n downloadUrl?: string;\n releaseNotes?: string;\n required: boolean;\n platform?: Platform;\n checksum?: string;\n sessionKey?: string;\n /** Set once the OTA plugin has downloaded the bundle. */\n bundleId?: string;\n}\n\n/**\n * Narrows a raw response into an update to act on, or `null` when there is\n * nothing to do.\n *\n * Native wins over OTA. The server can return both - a required native binary\n * alongside the OTA bundle that needs it - and installing the bundle first\n * would leave the device on a binary too old to run it.\n */\nexport function resolveUpdate(\n response: UpdateCheckResponse | null | undefined,\n): ResolvedUpdate | null {\n if (!response) return null;\n\n const native = response.native_update;\n if (native?.download_url) {\n return {\n kind: \"native\",\n version: native.version_name,\n versionCode: native.version_code,\n downloadUrl: native.download_url,\n releaseNotes: native.release_notes,\n // A native update is mandatory when the server says the OTA bundle\n // cannot run without it, whatever the record's own flag says.\n required:\n response.message === UpdateMessage.NATIVE_UPDATE_REQUIRED || (native.required ?? false),\n platform: native.platform,\n };\n }\n\n if (response.url && response.version_name) {\n return {\n kind: \"ota\",\n version: response.version_name,\n downloadUrl: response.url,\n releaseNotes: response.release_notes,\n required: response.required ?? false,\n checksum: response.checksum,\n sessionKey: response.sessionKey,\n };\n }\n\n return null;\n}\n\n/**\n * True when the response reports a condition the user cannot fix by updating.\n * Callers should surface these instead of showing \"you are up to date\".\n */\nexport function isBlockingResponse(response: UpdateCheckResponse): boolean {\n return (\n response.message === UpdateMessage.CHANNEL_NOT_FOUND ||\n response.message === UpdateMessage.ENVIRONMENT_MISMATCH\n );\n}\n\n/** Analytics events posted to `POST {endpoint}/api/native-updates/log`. */\nexport type UpdateEvent =\n | \"check\"\n | \"download\"\n | \"download_complete\"\n | \"install\"\n | \"cancel\"\n | \"error\";\n\nexport interface UpdateEventPayload {\n event: UpdateEvent;\n platform: Platform;\n device_id: string;\n current_version_code: number;\n new_version?: string;\n new_version_code?: number;\n channel: string;\n environment: string;\n error?: string;\n}\n","import type { Environment } from \"./update-contract.js\";\n\n/**\n * `.capucho/project.json` - the file that makes an application deployable.\n *\n * Version 1 held only the cloud identifiers, so the CLI had to *guess* how to\n * build: it shelled out to `pnpm run assets:<env>`, `pnpm build:<env>`,\n * `pnpm trapeze:<env>` and `pnpm exec cap sync`. Any application that named\n * its scripts differently, used npm, or had not installed Trapeze simply\n * failed halfway through a deploy.\n *\n * Version 2 describes the *inputs* instead - where each flavour's env file,\n * Trapeze config and icon sources live - and lets the CLI own the execution.\n * Every field has a default, so a v1 file keeps working: `normaliseProjectConfig`\n * fills in the conventional layout both existing apps already use.\n */\nexport const PROJECT_CONFIG_VERSION = 2;\n\n/** One build flavour, keyed by the environment its channels are bound to. */\nexport interface FlavourConfig {\n /**\n * Env file supplying `VITE_APP_ID`, `VITE_APP_NAME`, `VITE_UPDATE_CHANNEL`\n * and friends. The CLI reads it and passes the values to the build and to\n * the native configuration step as environment variables - it does not\n * rewrite the file, so a deploy leaves the working tree clean.\n */\n envFile: string;\n /** Trapeze config applied to the native projects. Optional. */\n trapezeConfig?: string;\n /** Directory holding `icon.png` / `splash.png` for icon generation. */\n assetPath?: string;\n /** Vite `--mode`. Defaults to the flavour name. */\n mode?: string;\n}\n\nexport interface BuildConfig {\n /**\n * Overrides the web build. Leave unset and the CLI runs the project's own\n * Vite build through the workspace toolchain it detects.\n */\n command?: string;\n /** Where to run the build from, relative to the app. For monorepo roots. */\n cwd?: string;\n}\n\nexport interface ProjectConfig {\n /** Absent on v1 files. */\n version?: number;\n\n /** Bundle identifier of the production flavour. */\n appId: string;\n /** Primary key of the application in Capucho. */\n cloudAppId: string;\n appName: string;\n createdAt: string;\n\n /** Vite output directory, and what Capacitor copies into the native app. */\n webDir?: string;\n androidDir?: string;\n iosDir?: string;\n\n /**\n * Monotonic native build numbers per environment. Written by the CLI, and\n * the one file a deploy is expected to modify.\n */\n versionCodeFile?: string;\n\n flavours?: Partial<Record<Environment, FlavourConfig>>;\n build?: BuildConfig;\n\n /** Optional GitHub Pages mirror for generated web assets. */\n ghPagesRepo?: string;\n\n /** @deprecated v1 fields, folded into `build` by `normaliseProjectConfig`. */\n monorepoRoot?: string;\n /** @deprecated v1 field. */\n packageName?: string;\n}\n\n/** A `ProjectConfig` with every optional resolved. */\nexport interface ResolvedProjectConfig {\n version: number;\n appId: string;\n cloudAppId: string;\n appName: string;\n createdAt: string;\n webDir: string;\n androidDir: string;\n iosDir: string;\n versionCodeFile: string;\n flavours: Record<Environment, FlavourConfig>;\n build: BuildConfig;\n ghPagesRepo?: string;\n}\n\nexport const ENVIRONMENTS: readonly Environment[] = [\"dev\", \"staging\", \"prod\"];\n\n/**\n * The layout both existing applications already use. Used as the default so a\n * v1 `project.json` needs no migration to keep deploying.\n */\nexport function defaultFlavour(environment: Environment): FlavourConfig {\n return {\n envFile: `build/${environment}/.env.${environment}`,\n trapezeConfig: `build/${environment}/trapeze.${environment}.yaml`,\n assetPath: `build/${environment}/assets`,\n mode: environment,\n };\n}\n\nexport function normaliseProjectConfig(config: ProjectConfig): ResolvedProjectConfig {\n const flavours = {} as Record<Environment, FlavourConfig>;\n for (const environment of ENVIRONMENTS) {\n const defaults = defaultFlavour(environment);\n const declared = config.flavours?.[environment];\n flavours[environment] = {\n envFile: declared?.envFile ?? defaults.envFile,\n trapezeConfig: declared?.trapezeConfig ?? defaults.trapezeConfig,\n assetPath: declared?.assetPath ?? defaults.assetPath,\n mode: declared?.mode ?? defaults.mode,\n };\n }\n\n // v1 expressed monorepo builds as `monorepoRoot` + `packageName`, which the\n // CLI turned into `pnpm exec vp run <pkg>#build:<env>`. Carry that forward as\n // an explicit build command so the behaviour is visible rather than implied.\n const build: BuildConfig = { ...config.build };\n if (!build.cwd && config.monorepoRoot) build.cwd = config.monorepoRoot;\n if (!build.command && config.packageName) {\n build.command = `vp run ${config.packageName}#build`;\n }\n\n return {\n version: config.version ?? 1,\n appId: config.appId,\n cloudAppId: config.cloudAppId,\n appName: config.appName,\n createdAt: config.createdAt,\n webDir: config.webDir ?? \"dist\",\n androidDir: config.androidDir ?? \"android\",\n iosDir: config.iosDir ?? \"ios\",\n versionCodeFile: config.versionCodeFile ?? \"version-code.json\",\n flavours,\n build,\n ghPagesRepo: config.ghPagesRepo,\n };\n}\n\n/** Fields a `project.json` must carry for a deploy to be possible. */\nexport function validateProjectConfig(config: Partial<ProjectConfig> | null | undefined): string[] {\n if (!config) return [\"project.json is missing or empty\"];\n\n const problems: string[] = [];\n if (!config.appId) problems.push(\"appId is required\");\n if (!config.cloudAppId) problems.push(\"cloudAppId is required\");\n if (!config.appName) problems.push(\"appName is required\");\n\n if (config.appId && !isValidBundleId(config.appId)) {\n problems.push(`appId \"${config.appId}\" is not a valid bundle identifier`);\n }\n\n return problems;\n}\n\nconst BUNDLE_ID = /^[a-z][a-z\\d_]*(\\.[a-z][a-z\\d_]*)+$/;\n\nexport function isValidBundleId(value: string): boolean {\n return BUNDLE_ID.test(value);\n}\n\n/**\n * Derives the environment a bundle identifier belongs to.\n *\n * The backend enforces the same rule server-side: a `.staging` build may only\n * be served staging channels. Mirroring it here lets the CLI refuse a\n * mismatched deploy before it uploads several megabytes.\n */\nexport function environmentFromAppId(appId: string): Environment {\n const id = appId.toLowerCase();\n if (id.endsWith(\".staging\")) return \"staging\";\n if (id.endsWith(\".dev\") || id.endsWith(\".debug\")) return \"dev\";\n return \"prod\";\n}\n\n/**\n * Whether a build may be served a channel bound to `channelEnvironment`.\n *\n * This mirrors the server's isolation check exactly, including its one\n * deliberate exception: a production build is allowed on a staging channel, so\n * a release candidate can be beta-tested by real installs without shipping a\n * separate bundle identifier.\n *\n * The rule lives here rather than being restated at each call site because the\n * CLI had reimplemented it as a plain equality check - which is *stricter* than\n * the server and rejected the exact beta-testing setup Lowmaro uses, where all\n * three channels are bound to staging and the app id carries no suffix.\n */\nexport function isEnvironmentAllowed(appId: string, channelEnvironment: Environment): boolean {\n const expected = environmentFromAppId(appId);\n if (expected === channelEnvironment) return true;\n return expected === \"prod\" && channelEnvironment === \"staging\";\n}\n\n/** Explains a rejected pairing, or null when it is allowed. */\nexport function describeEnvironmentMismatch(\n appId: string,\n channelEnvironment: Environment,\n channelName: string,\n): string | null {\n if (isEnvironmentAllowed(appId, channelEnvironment)) return null;\n\n const expected = environmentFromAppId(appId);\n return (\n `Channel \"${channelName}\" serves the ${channelEnvironment} environment, but ` +\n `the build's VITE_APP_ID is \"${appId}\", which is a ${expected} bundle id. ` +\n \"The server rejects this pairing, so the upload would be wasted.\"\n );\n}\n","import type { Environment } from \"./update-contract.js\";\n\n/**\n * Version arithmetic, kept free of any filesystem or child-process access so\n * both the CLI and the tests can use it directly.\n *\n * The CLI used to shell out to `npm version <type> --no-git-tag-version` for\n * this. In a workspace that is actively wrong: npm resolves the *nearest*\n * package.json, so running it from a monorepo root bumped the root package\n * instead of the app, and it mixed npm into a pnpm/Vite+ project for a job\n * that is three lines of string handling.\n */\n\nexport type BumpType = \"major\" | \"minor\" | \"patch\";\n\nexport interface SemanticVersion {\n major: number;\n minor: number;\n patch: number;\n prerelease?: string;\n build?: string;\n}\n\nconst SEMVER =\n /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([\\dA-Za-z-]+(?:\\.[\\dA-Za-z-]+)*))?(?:\\+([\\dA-Za-z-]+(?:\\.[\\dA-Za-z-]+)*))?$/;\n\nexport function parseVersion(value: string): SemanticVersion | null {\n const match = SEMVER.exec(value.trim());\n if (!match) return null;\n\n return {\n major: Number(match[1]),\n minor: Number(match[2]),\n patch: Number(match[3]),\n prerelease: match[4],\n build: match[5],\n };\n}\n\nexport function formatVersion(version: SemanticVersion): string {\n let out = `${version.major}.${version.minor}.${version.patch}`;\n if (version.prerelease) out += `-${version.prerelease}`;\n if (version.build) out += `+${version.build}`;\n return out;\n}\n\n/**\n * Bumps a version string. Prerelease and build metadata are dropped, matching\n * `npm version` semantics for a plain major/minor/patch bump.\n */\nexport function bumpVersion(value: string, type: BumpType): string {\n const parsed = parseVersion(value);\n if (!parsed) {\n throw new Error(`\"${value}\" is not a semantic version, so it cannot be bumped`);\n }\n\n switch (type) {\n case \"major\":\n return formatVersion({ major: parsed.major + 1, minor: 0, patch: 0 });\n case \"minor\":\n return formatVersion({\n major: parsed.major,\n minor: parsed.minor + 1,\n patch: 0,\n });\n case \"patch\":\n return formatVersion({\n major: parsed.major,\n minor: parsed.minor,\n patch: parsed.patch + 1,\n });\n }\n}\n\n/**\n * Compares two semantic versions. Returns a negative number when `a` is older.\n *\n * A missing or unparseable version sorts oldest, which is what the app needs:\n * the sentinel `\"builtin\"` must always look older than any published bundle.\n */\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a);\n const right = parseVersion(b);\n\n if (!left && !right) return 0;\n if (!left) return -1;\n if (!right) return 1;\n\n if (left.major !== right.major) return left.major - right.major;\n if (left.minor !== right.minor) return left.minor - right.minor;\n if (left.patch !== right.patch) return left.patch - right.patch;\n\n // 1.0.0-beta precedes 1.0.0.\n if (left.prerelease && !right.prerelease) return -1;\n if (!left.prerelease && right.prerelease) return 1;\n if (left.prerelease && right.prerelease) {\n return left.prerelease < right.prerelease ? -1 : left.prerelease > right.prerelease ? 1 : 0;\n }\n\n return 0;\n}\n\nexport type VersionCodes = Record<Environment, number>;\n\nexport const INITIAL_VERSION_CODES: VersionCodes = {\n dev: 1,\n staging: 1,\n prod: 1,\n};\n\n/**\n * Native build numbers must increase monotonically per environment: Android\n * refuses to install an APK whose versionCode is not greater than the\n * installed one, and the backend uses the same number to decide whether a\n * native update supersedes an OTA bundle.\n */\nexport function nextVersionCode(\n codes: Partial<VersionCodes> | null | undefined,\n environment: Environment,\n): VersionCodes {\n const current: VersionCodes = { ...INITIAL_VERSION_CODES, ...codes };\n return { ...current, [environment]: (current[environment] ?? 0) + 1 };\n}\n\n/**\n * Build-time variables injected into the web build and the native\n * configuration step.\n *\n * These used to be written *into* the committed `build/<env>/.env.<env>` file\n * by every deploy, which dirtied the working tree and made two concurrent\n * deploys race over one file. They are environment variables now: Trapeze\n * reads its `vars:` block from the process environment, and Vite reads\n * `VITE_*` the same way.\n */\nexport function versionEnv(version: string, versionCode: number) {\n return {\n VITE_APP_VERSION: version,\n VERSION_CODE: String(versionCode),\n BUILD_NUMBER: String(versionCode),\n };\n}\n","import type { Environment, Platform } from \"./update-contract.js\";\n\n/** Shapes returned by the authenticated `/api/*` endpoints. */\n\nexport interface CloudOrganization {\n id: string;\n name: string;\n slug: string;\n role: \"owner\" | \"admin\" | \"member\";\n}\n\nexport interface CloudApp {\n id: string;\n name: string;\n app_id: string;\n platform: string;\n organization_id: string;\n created_at: string;\n icon_url?: string;\n}\n\nexport interface CloudChannel {\n id: string;\n name: string;\n app_id: string;\n /**\n * Which build flavour this channel serves. The CLI derives the whole build\n * from it, so a channel without an environment cannot be deployed to.\n */\n environment: Environment;\n public: boolean;\n created_at: string;\n current_version_id?: string | null;\n current_native_version_id?: string | null;\n}\n\nexport interface CloudRelease {\n id: string;\n version_name: string;\n platform: Platform;\n channel?: string;\n active: boolean;\n required: boolean;\n release_notes?: string;\n created_at: string;\n}\n\nexport interface CloudUser {\n id: string;\n email: string;\n role?: string;\n}\n\n/** Response of `GET /api/auth/me`. */\nexport interface UserProfile {\n user: CloudUser;\n organizations: CloudOrganization[];\n apps: Array<CloudApp & { role: string }>;\n}\n\n/** Roles allowed to create an application inside an organization. */\nexport const APP_CREATOR_ROLES: ReadonlySet<string> = new Set([\"owner\", \"admin\"]);\n\nexport function canCreateApps(organization: CloudOrganization): boolean {\n return APP_CREATOR_ROLES.has(organization.role);\n}\n"],"mappings":"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"}
|