@expo-up/cli 0.1.3 → 0.1.5

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.
@@ -1,74 +0,0 @@
1
- /// <reference path="../../typescript-config/bun-test-shim.d.ts" />
2
- import { describe, expect, it } from "bun:test";
3
- import { parseBuildFolders, resolveRollbackSelection } from "./rollback-utils";
4
-
5
- describe("parseBuildFolders", () => {
6
- it("keeps only numeric dir names sorted desc", () => {
7
- const result = parseBuildFolders([
8
- { type: "file", name: "metadata.json" },
9
- { type: "dir", name: "12" },
10
- { type: "dir", name: "abc" },
11
- { type: "dir", name: "4" },
12
- ]);
13
-
14
- expect(result).toEqual([12, 4]);
15
- });
16
- });
17
-
18
- describe("resolveRollbackSelection", () => {
19
- it("returns embedded when explicit embedded flag is set", () => {
20
- const result = resolveRollbackSelection({
21
- embedded: true,
22
- builds: [8, 7, 6],
23
- liveBuildId: 8,
24
- embeddedTarget: "EMBEDDED",
25
- });
26
-
27
- expect(result.targetValue).toBe("EMBEDDED");
28
- expect(result.usedFallbackToEmbedded).toBe(false);
29
- });
30
-
31
- it("returns explicit numeric --to target", () => {
32
- const result = resolveRollbackSelection({
33
- to: "5",
34
- builds: [8, 7, 6],
35
- liveBuildId: 8,
36
- embeddedTarget: "EMBEDDED",
37
- });
38
-
39
- expect(result.targetValue).toBe("5");
40
- });
41
-
42
- it("falls back to previous build when available", () => {
43
- const result = resolveRollbackSelection({
44
- builds: [8, 7, 6],
45
- liveBuildId: 8,
46
- embeddedTarget: "EMBEDDED",
47
- });
48
-
49
- expect(result.targetValue).toBe("7");
50
- expect(result.usedFallbackToEmbedded).toBe(false);
51
- });
52
-
53
- it("falls back to embedded when no previous build exists", () => {
54
- const result = resolveRollbackSelection({
55
- builds: [8],
56
- liveBuildId: 8,
57
- embeddedTarget: "EMBEDDED",
58
- });
59
-
60
- expect(result.targetValue).toBe("EMBEDDED");
61
- expect(result.usedFallbackToEmbedded).toBe(true);
62
- });
63
-
64
- it("throws for non-numeric --to values", () => {
65
- expect(() =>
66
- resolveRollbackSelection({
67
- to: "latest",
68
- builds: [8, 7],
69
- liveBuildId: 8,
70
- embeddedTarget: "EMBEDDED",
71
- }),
72
- ).toThrow('Invalid rollback target "latest"');
73
- });
74
- });
@@ -1,53 +0,0 @@
1
- export interface RollbackSelectionInput {
2
- embedded?: boolean;
3
- to?: string;
4
- builds: number[];
5
- liveBuildId: number;
6
- embeddedTarget: string;
7
- }
8
-
9
- export interface RollbackSelectionResult {
10
- targetValue: string;
11
- usedFallbackToEmbedded: boolean;
12
- }
13
-
14
- export function parseBuildFolders(
15
- items: Array<{ type?: string; name?: string }>,
16
- ): number[] {
17
- return items
18
- .filter((item) => item.type === "dir" && typeof item.name === "string")
19
- .map((item) => Number.parseInt(item.name as string, 10))
20
- .filter((value) => Number.isFinite(value))
21
- .sort((a, b) => b - a);
22
- }
23
-
24
- export function resolveRollbackSelection(
25
- input: RollbackSelectionInput,
26
- ): RollbackSelectionResult {
27
- const { embedded, to, builds, liveBuildId, embeddedTarget } = input;
28
-
29
- if (embedded) {
30
- return { targetValue: embeddedTarget, usedFallbackToEmbedded: false };
31
- }
32
-
33
- if (to) {
34
- const numericTarget = Number.parseInt(to, 10);
35
- if (!Number.isFinite(numericTarget)) {
36
- throw new Error(
37
- `Invalid rollback target "${to}". Use a numeric build ID or --embedded.`,
38
- );
39
- }
40
-
41
- return { targetValue: `${numericTarget}`, usedFallbackToEmbedded: false };
42
- }
43
-
44
- const liveIndex = builds.indexOf(liveBuildId);
45
- if (liveIndex === -1 || liveIndex === builds.length - 1) {
46
- return { targetValue: embeddedTarget, usedFallbackToEmbedded: true };
47
- }
48
-
49
- return {
50
- targetValue: `${builds[liveIndex + 1]}`,
51
- usedFallbackToEmbedded: false,
52
- };
53
- }
package/src/rollback.tsx DELETED
@@ -1,267 +0,0 @@
1
- import * as React from "react";
2
- import { Text, Box, Static } from "ink";
3
- import Spinner from "ink-spinner";
4
- import { Octokit } from "@octokit/rest";
5
- import pc from "picocolors";
6
- import { getStoredToken, getAutoConfig } from "./auth";
7
- import {
8
- EMBEDDED_ROLLBACK_TARGET,
9
- parseProjectDescriptor,
10
- resolveRollbackTarget,
11
- } from "../../core/src/index";
12
- import { Badge, BrandHeader, CliCard, KV } from "./ui";
13
- import { parseBuildFolders, resolveRollbackSelection } from "./rollback-utils";
14
-
15
- interface RollbackProps {
16
- channel: string;
17
- to?: string;
18
- embedded?: boolean;
19
- debug?: boolean;
20
- }
21
-
22
- export const Rollback: React.FC<RollbackProps> = ({
23
- channel,
24
- to,
25
- embedded,
26
- debug = false,
27
- }) => {
28
- const [logs, setLogs] = React.useState<string[]>([]);
29
- const [debugLogs, setDebugLogs] = React.useState<string[]>([]);
30
- const [status, setStatus] = React.useState<
31
- "idle" | "running" | "success" | "error" | "skipped"
32
- >("idle");
33
- const [error, setError] = React.useState<string | null>(null);
34
- const [target, setTarget] = React.useState<string>("");
35
-
36
- React.useEffect(() => {
37
- const run = async () => {
38
- try {
39
- const appendLog = (message: string): void =>
40
- setLogs((prev) => [...prev, message]);
41
- const appendDebug = (message: string): void =>
42
- setDebugLogs((prev) => [...prev, message]);
43
- const token = getStoredToken();
44
- const { serverUrl, projectId, runtimeVersion } = getAutoConfig();
45
-
46
- if (!token || !serverUrl || !projectId || !runtimeVersion)
47
- throw new Error("Missing configuration.");
48
- if (debug)
49
- appendDebug(
50
- `Resolved config: server=${serverUrl}, project=${projectId}, runtime=${runtimeVersion}`,
51
- );
52
-
53
- const octokit = new Octokit({ auth: token });
54
- setStatus("running");
55
-
56
- const projRes = await fetch(`${serverUrl}/projects/${projectId}`);
57
- if (!projRes.ok) throw new Error(`Project "${projectId}" not found.`);
58
- const { owner, repo } = parseProjectDescriptor(await projRes.json());
59
-
60
- const { data: contents } = await octokit.repos.getContent({
61
- owner,
62
- repo,
63
- path: runtimeVersion,
64
- ref: channel,
65
- });
66
- if (!Array.isArray(contents)) throw new Error("No builds found.");
67
-
68
- const builds = parseBuildFolders(
69
- contents as Array<{ type?: string; name?: string }>,
70
- );
71
- const latestFolder = builds[0];
72
- if (debug)
73
- appendDebug(
74
- `Detected build folders: ${builds.join(", ") || "(none)"}`,
75
- );
76
-
77
- // 1. Resolve Target
78
- const resolvedLiveBuild = await resolveRollbackTarget({
79
- latestBuildId: latestFolder,
80
- loadRollbackTarget: async (buildId) => {
81
- try {
82
- const { data: file } = (await octokit.repos.getContent({
83
- owner,
84
- repo,
85
- path: `${runtimeVersion}/${buildId}/rollback`,
86
- ref: channel,
87
- })) as { data: { content: string } };
88
- return Buffer.from(file.content, "base64").toString();
89
- } catch {
90
- return null;
91
- }
92
- },
93
- });
94
- const liveBuildId = resolvedLiveBuild.buildId;
95
- if (debug) appendDebug(`Resolved live build id: ${liveBuildId}`);
96
-
97
- const { targetValue, usedFallbackToEmbedded } =
98
- resolveRollbackSelection({
99
- embedded,
100
- to,
101
- builds,
102
- liveBuildId,
103
- embeddedTarget: EMBEDDED_ROLLBACK_TARGET,
104
- });
105
-
106
- if (usedFallbackToEmbedded) {
107
- appendLog(
108
- `${pc.yellow("ℹ")} No previous build found. Falling back to ${EMBEDDED_ROLLBACK_TARGET}.`,
109
- );
110
- }
111
-
112
- setTarget(targetValue);
113
- if (debug) appendDebug(`Selected rollback target: ${targetValue}`);
114
-
115
- // 2. Check Duplicate
116
- try {
117
- const { data: lastFile } = (await octokit.repos.getContent({
118
- owner,
119
- repo,
120
- path: `${runtimeVersion}/${latestFolder}/rollback`,
121
- ref: channel,
122
- })) as any;
123
- if (
124
- Buffer.from(lastFile.content, "base64").toString().trim() ===
125
- targetValue
126
- ) {
127
- setStatus("skipped");
128
- appendLog(
129
- `${pc.yellow("⚠")} Already rolled back to ${targetValue}.`,
130
- );
131
- return;
132
- }
133
- } catch (e) {}
134
-
135
- const nextBuildFolder = latestFolder + 1;
136
-
137
- // 3. Commit
138
- const { data: targetRef } = await octokit.git.getRef({
139
- owner,
140
- repo,
141
- ref: `heads/${channel}`,
142
- });
143
- const { data: blob } = await octokit.git.createBlob({
144
- owner,
145
- repo,
146
- content: targetValue,
147
- encoding: "utf-8",
148
- });
149
- const { data: baseCommit } = await octokit.git.getCommit({
150
- owner,
151
- repo,
152
- commit_sha: targetRef.object.sha,
153
- });
154
- const { data: tree } = await octokit.git.createTree({
155
- owner,
156
- repo,
157
- base_tree: baseCommit.tree.sha,
158
- tree: [
159
- {
160
- path: `${runtimeVersion}/${nextBuildFolder}/rollback`,
161
- mode: "100644",
162
- type: "blob",
163
- sha: blob.sha,
164
- },
165
- ],
166
- });
167
- const { data: commit } = await octokit.git.createCommit({
168
- owner,
169
- repo,
170
- message: `rollback: to ${targetValue} on ${channel}`,
171
- tree: tree.sha,
172
- parents: [targetRef.object.sha],
173
- });
174
- await octokit.git.updateRef({
175
- owner,
176
- repo,
177
- ref: `heads/${channel}`,
178
- sha: commit.sha,
179
- });
180
- if (debug) appendDebug(`Created rollback commit: ${commit.sha}`);
181
-
182
- setStatus("success");
183
- appendLog(
184
- `${pc.green("✨")} Successfully rolled back to ${targetValue}!`,
185
- );
186
- } catch (e: any) {
187
- setStatus("error");
188
- setError(e.message);
189
- if (debug) setDebugLogs((prev) => [...prev, `Failure: ${e.message}`]);
190
- }
191
- };
192
- run();
193
- }, [channel, debug, embedded, to]);
194
-
195
- return (
196
- <Box flexDirection="column" padding={1}>
197
- <BrandHeader subtitle="Over-the-air updates" />
198
- <CliCard
199
- title="expo-up rollback"
200
- subtitle="Move channel to a previous build or embedded"
201
- >
202
- <KV keyName="Channel" value={channel} valueColor="cyan" />
203
- <KV
204
- keyName="Target"
205
- value={embedded ? EMBEDDED_ROLLBACK_TARGET : (to ?? "auto")}
206
- valueColor="yellow"
207
- />
208
- </CliCard>
209
-
210
- <CliCard title="Progress">
211
- <Static items={logs}>
212
- {(log, i) => <Text key={i}>{`• ${log}`}</Text>}
213
- </Static>
214
- {status === "running" && (
215
- <Box marginTop={1}>
216
- <Badge label="RUNNING" tone="yellow" />
217
- <Text>
218
- <Spinner /> Applying rollback...
219
- </Text>
220
- </Box>
221
- )}
222
- {status === "success" && (
223
- <Box marginTop={1}>
224
- <Badge label="SUCCESS" tone="green" />
225
- <Text color="green">Rollback complete.</Text>
226
- </Box>
227
- )}
228
- {status === "skipped" && (
229
- <Box marginTop={1} flexDirection="column">
230
- <Box>
231
- <Badge label="SKIPPED" tone="yellow" />
232
- <Text color="yellow">Already at target.</Text>
233
- </Box>
234
- <Text dimColor>
235
- The live version on {pc.cyan(channel)} is already{" "}
236
- {pc.white(
237
- target === EMBEDDED_ROLLBACK_TARGET
238
- ? "the Native Build"
239
- : "Build " + target,
240
- )}
241
- .
242
- </Text>
243
- </Box>
244
- )}
245
- {status === "error" && (
246
- <Box marginTop={1}>
247
- <Badge label="FAILED" tone="red" />
248
- <Text color="red">{error}</Text>
249
- </Box>
250
- )}
251
- </CliCard>
252
-
253
- {debug && (
254
- <CliCard title="Debug Logs" subtitle="Verbose diagnostics">
255
- {debugLogs.length === 0 ? (
256
- <Text color="gray">No debug logs yet.</Text>
257
- ) : null}
258
- {debugLogs.map((log, i) => (
259
- <Text key={i} color="gray">
260
- {log}
261
- </Text>
262
- ))}
263
- </CliCard>
264
- )}
265
- </Box>
266
- );
267
- };
package/src/ui.tsx DELETED
@@ -1,99 +0,0 @@
1
- import * as React from "react";
2
- import { Box, Text } from "ink";
3
- import figlet from "figlet";
4
-
5
- type Tone =
6
- | "cyan"
7
- | "green"
8
- | "yellow"
9
- | "red"
10
- | "blue"
11
- | "magenta"
12
- | "gray"
13
- | "white";
14
-
15
- interface CliCardProps {
16
- title: string;
17
- subtitle?: string;
18
- children: React.ReactNode;
19
- }
20
-
21
- interface BadgeProps {
22
- label: string;
23
- tone?: Tone;
24
- }
25
-
26
- const EXPO_UP_BANNER = (() => {
27
- try {
28
- const figletApi =
29
- (figlet as unknown as { default?: typeof figlet }).default ?? figlet;
30
- return figletApi
31
- .textSync("expo-up", { font: "Standard" })
32
- .split("\n")
33
- .filter(Boolean);
34
- } catch {
35
- return ["expo-up"];
36
- }
37
- })();
38
-
39
- export const BrandHeader: React.FC<{ subtitle?: string }> = ({ subtitle }) => {
40
- return (
41
- <Box flexDirection="column" marginBottom={1}>
42
- {EXPO_UP_BANNER.map((line) => (
43
- <Text key={line} color="cyan">
44
- {line}
45
- </Text>
46
- ))}
47
- {subtitle ? <Text color="gray">{subtitle}</Text> : null}
48
- </Box>
49
- );
50
- };
51
-
52
- export const CliCard: React.FC<CliCardProps> = ({
53
- title,
54
- subtitle,
55
- children,
56
- }) => {
57
- return (
58
- <Box
59
- flexDirection="column"
60
- borderStyle="round"
61
- borderColor="cyan"
62
- paddingX={1}
63
- paddingY={0}
64
- >
65
- <Box>
66
- <Text color="cyan" bold>
67
- {title}
68
- </Text>
69
- </Box>
70
- {subtitle ? (
71
- <Box marginBottom={1}>
72
- <Text color="gray">{subtitle}</Text>
73
- </Box>
74
- ) : null}
75
- {children}
76
- </Box>
77
- );
78
- };
79
-
80
- export const Badge: React.FC<BadgeProps> = ({ label, tone = "white" }) => {
81
- return (
82
- <Box marginRight={1}>
83
- <Text color={tone}>{`[${label}]`}</Text>
84
- </Box>
85
- );
86
- };
87
-
88
- export const KV: React.FC<{
89
- keyName: string;
90
- value: string;
91
- valueColor?: Tone;
92
- }> = ({ keyName, value, valueColor = "white" }) => {
93
- return (
94
- <Box>
95
- <Text color="gray">{keyName.padEnd(10)}</Text>
96
- <Text color={valueColor}>{value}</Text>
97
- </Box>
98
- );
99
- };
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "extends": "../typescript-config/base.json",
3
- "compilerOptions": {
4
- "jsx": "react-jsx",
5
- "lib": ["ESNext", "DOM"],
6
- "outDir": "./dist",
7
- "declaration": true,
8
- "emitDeclarationOnly": true
9
- },
10
- "include": ["src/**/*"],
11
- "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
12
- }