@apptile/tile-cli 0.1.0-beta.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.
Files changed (3) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +4469 -0
  3. package/package.json +41 -0
package/dist/index.js ADDED
@@ -0,0 +1,4469 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJS = (cb, mod) => function __require() {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2,
26
+ mod
27
+ ));
28
+
29
+ // ../core/dist/output.js
30
+ var require_output = __commonJS({
31
+ "../core/dist/output.js"(exports2) {
32
+ "use strict";
33
+ Object.defineProperty(exports2, "__esModule", { value: true });
34
+ exports2.ExitCode = void 0;
35
+ exports2.setJsonMode = setJsonMode2;
36
+ exports2.isJsonMode = isJsonMode12;
37
+ exports2.out = out12;
38
+ exports2.hint = hint13;
39
+ exports2.info = info12;
40
+ exports2.ExitCode = {
41
+ OK: 0,
42
+ ERROR: 1,
43
+ // generic failure
44
+ USAGE: 2,
45
+ // bad arguments (commander also uses 1; we use 2 for our own checks)
46
+ AUTH: 3,
47
+ // not authenticated / 401 / 403
48
+ NOT_FOUND: 4,
49
+ // 404
50
+ CONFLICT: 5,
51
+ // 409 (e.g. optimistic-lock failure)
52
+ NETWORK: 6,
53
+ // could not reach the backend
54
+ UNSUPPORTED: 7
55
+ // capability not available for this project (e.g. no test flows configured)
56
+ };
57
+ var jsonMode = false;
58
+ function setJsonMode2(on) {
59
+ jsonMode = on;
60
+ }
61
+ function isJsonMode12() {
62
+ return jsonMode;
63
+ }
64
+ function out12(value) {
65
+ if (jsonMode) {
66
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
67
+ return;
68
+ }
69
+ if (typeof value === "string") {
70
+ process.stdout.write(value + "\n");
71
+ return;
72
+ }
73
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
74
+ }
75
+ function hint13(...lines) {
76
+ if (jsonMode || lines.length === 0)
77
+ return;
78
+ for (const line of lines) {
79
+ process.stderr.write(line + "\n");
80
+ }
81
+ }
82
+ function info12(line) {
83
+ if (jsonMode)
84
+ return;
85
+ process.stderr.write(line + "\n");
86
+ }
87
+ }
88
+ });
89
+
90
+ // ../core/dist/errors.js
91
+ var require_errors = __commonJS({
92
+ "../core/dist/errors.js"(exports2) {
93
+ "use strict";
94
+ Object.defineProperty(exports2, "__esModule", { value: true });
95
+ exports2.TileError = void 0;
96
+ exports2.exitCodeForStatus = exitCodeForStatus;
97
+ var output_1 = require_output();
98
+ var TileError10 = class extends Error {
99
+ constructor(message, exitCode = output_1.ExitCode.ERROR, nextStep) {
100
+ super(message);
101
+ this.name = "TileError";
102
+ this.exitCode = exitCode;
103
+ this.nextStep = nextStep;
104
+ }
105
+ };
106
+ exports2.TileError = TileError10;
107
+ function exitCodeForStatus(status) {
108
+ switch (status) {
109
+ case 401:
110
+ case 403:
111
+ return output_1.ExitCode.AUTH;
112
+ case 404:
113
+ return output_1.ExitCode.NOT_FOUND;
114
+ case 409:
115
+ return output_1.ExitCode.CONFLICT;
116
+ default:
117
+ return output_1.ExitCode.ERROR;
118
+ }
119
+ }
120
+ }
121
+ });
122
+
123
+ // ../core/dist/auth.js
124
+ var require_auth = __commonJS({
125
+ "../core/dist/auth.js"(exports2) {
126
+ "use strict";
127
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
128
+ if (k2 === void 0) k2 = k;
129
+ var desc = Object.getOwnPropertyDescriptor(m, k);
130
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
131
+ desc = { enumerable: true, get: function() {
132
+ return m[k];
133
+ } };
134
+ }
135
+ Object.defineProperty(o, k2, desc);
136
+ }) : (function(o, m, k, k2) {
137
+ if (k2 === void 0) k2 = k;
138
+ o[k2] = m[k];
139
+ }));
140
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
141
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
142
+ }) : function(o, v) {
143
+ o["default"] = v;
144
+ });
145
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
146
+ var ownKeys = function(o) {
147
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
148
+ var ar = [];
149
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
150
+ return ar;
151
+ };
152
+ return ownKeys(o);
153
+ };
154
+ return function(mod) {
155
+ if (mod && mod.__esModule) return mod;
156
+ var result = {};
157
+ if (mod != null) {
158
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
159
+ }
160
+ __setModuleDefault(result, mod);
161
+ return result;
162
+ };
163
+ })();
164
+ Object.defineProperty(exports2, "__esModule", { value: true });
165
+ exports2.readCredentials = readCredentials;
166
+ exports2.getToken = getToken3;
167
+ exports2.requireToken = requireToken;
168
+ exports2.login = login2;
169
+ exports2.register = register2;
170
+ exports2.logout = logout2;
171
+ exports2.whoami = whoami3;
172
+ var fs8 = __importStar(require("fs"));
173
+ var os3 = __importStar(require("os"));
174
+ var path11 = __importStar(require("path"));
175
+ var http_1 = require_http();
176
+ var errors_1 = require_errors();
177
+ var output_1 = require_output();
178
+ function configDir() {
179
+ const base = process.env.XDG_CONFIG_HOME || path11.join(os3.homedir(), ".config");
180
+ return path11.join(base, "tile");
181
+ }
182
+ function credsPath() {
183
+ return path11.join(configDir(), "credentials.json");
184
+ }
185
+ function readCredentials() {
186
+ try {
187
+ const raw = fs8.readFileSync(credsPath(), "utf8");
188
+ return JSON.parse(raw);
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+ function writeCredentials(creds) {
194
+ const dir = configDir();
195
+ fs8.mkdirSync(dir, { recursive: true });
196
+ fs8.writeFileSync(credsPath(), JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
197
+ }
198
+ function getToken3() {
199
+ if (process.env.TILE_TOKEN)
200
+ return process.env.TILE_TOKEN;
201
+ return readCredentials()?.token;
202
+ }
203
+ function requireToken() {
204
+ const t = getToken3();
205
+ if (!t) {
206
+ throw new errors_1.TileError("Not authenticated.", output_1.ExitCode.AUTH, "Run `tile login` or set TILE_TOKEN.");
207
+ }
208
+ return t;
209
+ }
210
+ async function login2(email, password) {
211
+ const res = await (0, http_1.request)("/api/auth/login", {
212
+ method: "POST",
213
+ body: { email, password },
214
+ token: null
215
+ });
216
+ const creds = { token: res.token, user: res.user, apiUrl: process.env.TILE_API_URL };
217
+ writeCredentials(creds);
218
+ return creds;
219
+ }
220
+ async function register2(email, password, name) {
221
+ const res = await (0, http_1.request)("/api/auth/register", {
222
+ method: "POST",
223
+ body: { email, password, name },
224
+ token: null
225
+ });
226
+ const creds = { token: res.token, user: res.user, apiUrl: process.env.TILE_API_URL };
227
+ writeCredentials(creds);
228
+ return creds;
229
+ }
230
+ function logout2() {
231
+ try {
232
+ fs8.unlinkSync(credsPath());
233
+ } catch {
234
+ }
235
+ }
236
+ async function whoami3() {
237
+ requireToken();
238
+ const res = await (0, http_1.request)("/api/auth/me");
239
+ return res.user;
240
+ }
241
+ }
242
+ });
243
+
244
+ // ../core/dist/http.js
245
+ var require_http = __commonJS({
246
+ "../core/dist/http.js"(exports2) {
247
+ "use strict";
248
+ Object.defineProperty(exports2, "__esModule", { value: true });
249
+ exports2.baseUrl = baseUrl;
250
+ exports2.request = request;
251
+ var errors_1 = require_errors();
252
+ var output_1 = require_output();
253
+ var auth_1 = require_auth();
254
+ function baseUrl() {
255
+ return (process.env.TILE_API_URL || "https://demo-setup.tile.dev").replace(/\/+$/, "");
256
+ }
257
+ function buildUrl(path11, query) {
258
+ const url = new URL(path11.startsWith("http") ? path11 : baseUrl() + path11);
259
+ if (query) {
260
+ for (const [k, v] of Object.entries(query)) {
261
+ if (v !== void 0)
262
+ url.searchParams.set(k, String(v));
263
+ }
264
+ }
265
+ return url.toString();
266
+ }
267
+ async function request(path11, opts = {}) {
268
+ const { method = "GET", body, headers = {}, query } = opts;
269
+ const token = opts.token === null ? void 0 : opts.token ?? (0, auth_1.getToken)();
270
+ const finalHeaders = {
271
+ Accept: "application/json",
272
+ ...headers
273
+ };
274
+ if (body !== void 0)
275
+ finalHeaders["Content-Type"] = "application/json";
276
+ if (token) {
277
+ finalHeaders["Cookie"] = `tile_session=${token}`;
278
+ finalHeaders["Authorization"] = `Bearer ${token}`;
279
+ }
280
+ const url = buildUrl(path11, query);
281
+ let res;
282
+ try {
283
+ res = await fetch(url, {
284
+ method,
285
+ headers: finalHeaders,
286
+ body: body === void 0 ? void 0 : JSON.stringify(body)
287
+ });
288
+ } catch (e) {
289
+ throw new errors_1.TileError(`Could not reach the tile backend at ${baseUrl()}. Is the gateway up? (${e.message})`, output_1.ExitCode.NETWORK, "Start the dev stack, or set TILE_API_URL to the right host.");
290
+ }
291
+ const text = await res.text();
292
+ let parsed = void 0;
293
+ if (text) {
294
+ try {
295
+ parsed = JSON.parse(text);
296
+ } catch {
297
+ parsed = text;
298
+ }
299
+ }
300
+ if (!res.ok) {
301
+ const serverMsg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : typeof parsed === "string" && parsed ? parsed : res.statusText;
302
+ const code = (0, errors_1.exitCodeForStatus)(res.status);
303
+ let nextStep;
304
+ if (res.status === 401 || res.status === 403)
305
+ nextStep = "Run `tile login` (or set TILE_TOKEN).";
306
+ const err = new errors_1.TileError(`${res.status} ${serverMsg}`, code, nextStep);
307
+ err.status = res.status;
308
+ err.bodyJson = parsed;
309
+ throw err;
310
+ }
311
+ if (opts.raw)
312
+ return text;
313
+ return parsed;
314
+ }
315
+ }
316
+ });
317
+
318
+ // ../core/dist/config.js
319
+ var require_config = __commonJS({
320
+ "../core/dist/config.js"(exports2) {
321
+ "use strict";
322
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
323
+ if (k2 === void 0) k2 = k;
324
+ var desc = Object.getOwnPropertyDescriptor(m, k);
325
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
326
+ desc = { enumerable: true, get: function() {
327
+ return m[k];
328
+ } };
329
+ }
330
+ Object.defineProperty(o, k2, desc);
331
+ }) : (function(o, m, k, k2) {
332
+ if (k2 === void 0) k2 = k;
333
+ o[k2] = m[k];
334
+ }));
335
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
336
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
337
+ }) : function(o, v) {
338
+ o["default"] = v;
339
+ });
340
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
341
+ var ownKeys = function(o) {
342
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
343
+ var ar = [];
344
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
345
+ return ar;
346
+ };
347
+ return ownKeys(o);
348
+ };
349
+ return function(mod) {
350
+ if (mod && mod.__esModule) return mod;
351
+ var result = {};
352
+ if (mod != null) {
353
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
354
+ }
355
+ __setModuleDefault(result, mod);
356
+ return result;
357
+ };
358
+ })();
359
+ Object.defineProperty(exports2, "__esModule", { value: true });
360
+ exports2.readGlobalConfig = readGlobalConfig;
361
+ exports2.writeGlobalConfig = writeGlobalConfig3;
362
+ exports2.findConfigPath = findConfigPath3;
363
+ exports2.readConfig = readConfig3;
364
+ exports2.writeConfig = writeConfig3;
365
+ exports2.readProjectRegistry = readProjectRegistry2;
366
+ exports2.linkProject = linkProject3;
367
+ exports2.unlinkProject = unlinkProject2;
368
+ exports2.resolveProjectPath = resolveProjectPath2;
369
+ exports2.resolveApp = resolveApp8;
370
+ exports2.resolveEnv = resolveEnv2;
371
+ exports2.resolveAppOptional = resolveAppOptional3;
372
+ exports2.resolveEnvOptional = resolveEnvOptional2;
373
+ exports2.resolveProjectDir = resolveProjectDir2;
374
+ var fs8 = __importStar(require("fs"));
375
+ var os3 = __importStar(require("os"));
376
+ var path11 = __importStar(require("path"));
377
+ var errors_1 = require_errors();
378
+ var output_1 = require_output();
379
+ var FILE = "tile.json";
380
+ function globalConfigPath() {
381
+ const base = process.env.XDG_CONFIG_HOME || path11.join(os3.homedir(), ".config");
382
+ return path11.join(base, "tile", "config.json");
383
+ }
384
+ function readGlobalConfig() {
385
+ try {
386
+ return JSON.parse(fs8.readFileSync(globalConfigPath(), "utf8"));
387
+ } catch {
388
+ return {};
389
+ }
390
+ }
391
+ function writeGlobalConfig3(patch) {
392
+ const p = globalConfigPath();
393
+ fs8.mkdirSync(path11.dirname(p), { recursive: true });
394
+ const merged = { ...readGlobalConfig(), ...patch };
395
+ fs8.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n");
396
+ return p;
397
+ }
398
+ function findConfigPath3(start = process.cwd()) {
399
+ let dir = path11.resolve(start);
400
+ while (true) {
401
+ const candidate = path11.join(dir, FILE);
402
+ if (fs8.existsSync(candidate))
403
+ return candidate;
404
+ const parent = path11.dirname(dir);
405
+ if (parent === dir)
406
+ return null;
407
+ dir = parent;
408
+ }
409
+ }
410
+ function readConfig3(start) {
411
+ const p = findConfigPath3(start);
412
+ if (!p)
413
+ return {};
414
+ try {
415
+ return JSON.parse(fs8.readFileSync(p, "utf8"));
416
+ } catch {
417
+ return {};
418
+ }
419
+ }
420
+ function writeConfig3(config, dir = process.cwd()) {
421
+ const p = path11.join(dir, FILE);
422
+ fs8.writeFileSync(p, JSON.stringify(config, null, 2) + "\n");
423
+ return p;
424
+ }
425
+ function projectsPath() {
426
+ const base = process.env.XDG_CONFIG_HOME || path11.join(os3.homedir(), ".config");
427
+ return path11.join(base, "tile", "projects.json");
428
+ }
429
+ function readProjectRegistry2() {
430
+ try {
431
+ return JSON.parse(fs8.readFileSync(projectsPath(), "utf8"));
432
+ } catch {
433
+ return {};
434
+ }
435
+ }
436
+ function writeProjectRegistry(reg) {
437
+ const p = projectsPath();
438
+ fs8.mkdirSync(path11.dirname(p), { recursive: true });
439
+ fs8.writeFileSync(p, JSON.stringify(reg, null, 2) + "\n");
440
+ }
441
+ function linkProject3(appId, dir, name) {
442
+ const reg = readProjectRegistry2();
443
+ const now = (/* @__PURE__ */ new Date()).toISOString();
444
+ const prev = reg[appId];
445
+ reg[appId] = {
446
+ path: path11.resolve(dir),
447
+ name: name ?? prev?.name,
448
+ linkedAt: prev?.linkedAt ?? now,
449
+ lastUsedAt: now
450
+ };
451
+ writeProjectRegistry(reg);
452
+ }
453
+ function unlinkProject2(appId) {
454
+ const reg = readProjectRegistry2();
455
+ if (!reg[appId])
456
+ return false;
457
+ delete reg[appId];
458
+ writeProjectRegistry(reg);
459
+ return true;
460
+ }
461
+ function resolveProjectPath2(appId) {
462
+ const reg = readProjectRegistry2();
463
+ const link = reg[appId];
464
+ if (!link)
465
+ return void 0;
466
+ if (!fs8.existsSync(link.path)) {
467
+ delete reg[appId];
468
+ try {
469
+ writeProjectRegistry(reg);
470
+ } catch {
471
+ }
472
+ return void 0;
473
+ }
474
+ return link.path;
475
+ }
476
+ function resolveApp8(opts = {}) {
477
+ const app = opts.app || readConfig3().appId || process.env.TILE_APP || readGlobalConfig().appId;
478
+ if (!app) {
479
+ throw new errors_1.TileError("No app specified.", output_1.ExitCode.USAGE, "Run `tile use <appId>`, pass --app, add it to tile.json, or set TILE_APP.");
480
+ }
481
+ return app;
482
+ }
483
+ function resolveEnv2(opts = {}) {
484
+ const env = opts.env || readConfig3().env || process.env.TILE_ENV || readGlobalConfig().env;
485
+ if (!env) {
486
+ throw new errors_1.TileError("No environment specified.", output_1.ExitCode.USAGE, "Pass --env <env>, add it to tile.json, or set TILE_ENV.");
487
+ }
488
+ return env;
489
+ }
490
+ function resolveAppOptional3(opts = {}) {
491
+ return opts.app || readConfig3().appId || process.env.TILE_APP || readGlobalConfig().appId;
492
+ }
493
+ function resolveEnvOptional2(opts = {}) {
494
+ return opts.env || readConfig3().env || process.env.TILE_ENV || readGlobalConfig().env;
495
+ }
496
+ function resolveProjectDir2(opts = {}) {
497
+ const cfgPath = findConfigPath3();
498
+ if (cfgPath)
499
+ return path11.dirname(cfgPath);
500
+ const appId = opts.app || process.env.TILE_APP || readGlobalConfig().appId;
501
+ if (appId) {
502
+ const p = resolveProjectPath2(appId);
503
+ if (p) {
504
+ linkProject3(appId, p);
505
+ return p;
506
+ }
507
+ throw new errors_1.TileError(`No local repo is linked for app ${appId} on this machine (it may have moved).`, output_1.ExitCode.USAGE, `cd into its folder, or run \`tile pull --app ${appId}\` to fetch it.`);
508
+ }
509
+ throw new errors_1.TileError("Not inside a Tile app and no app selected.", output_1.ExitCode.USAGE, "cd into a Tile app, run `tile use <appId>`, or pass --app.");
510
+ }
511
+ }
512
+ });
513
+
514
+ // ../core/dist/liveLayer.js
515
+ var require_liveLayer = __commonJS({
516
+ "../core/dist/liveLayer.js"(exports2) {
517
+ "use strict";
518
+ Object.defineProperty(exports2, "__esModule", { value: true });
519
+ exports2.getCurrent = getCurrent2;
520
+ exports2.publish = publish;
521
+ exports2.listVersions = listVersions2;
522
+ exports2.getVersion = getVersion;
523
+ exports2.rollback = rollback;
524
+ exports2.listLayers = listLayers2;
525
+ exports2.deleteLayer = deleteLayer2;
526
+ exports2.publishMutating = publishMutating2;
527
+ exports2.parseCategoryArg = parseCategoryArg2;
528
+ exports2.setInPath = setInPath2;
529
+ exports2.unsetInPath = unsetInPath2;
530
+ exports2.getInPath = getInPath2;
531
+ exports2.parseValue = parseValue2;
532
+ var http_1 = require_http();
533
+ var enc = encodeURIComponent;
534
+ function catQuery(category) {
535
+ return category ? { category } : void 0;
536
+ }
537
+ function getCurrent2(app, env, category = "") {
538
+ return (0, http_1.request)(`/api/apps/${enc(app)}/live-layer/${enc(env)}`, {
539
+ query: catQuery(category)
540
+ });
541
+ }
542
+ function publish(app, env, config, baseVersion, category = "", note) {
543
+ return (0, http_1.request)(`/api/apps/${enc(app)}/live-layer/${enc(env)}/publish`, {
544
+ method: "POST",
545
+ body: { config, baseVersion, category, note }
546
+ });
547
+ }
548
+ async function listVersions2(app, env, category = "") {
549
+ const res = await (0, http_1.request)(`/api/apps/${enc(app)}/live-layer/${enc(env)}/versions`, { query: catQuery(category) });
550
+ return res.versions || [];
551
+ }
552
+ function getVersion(app, env, version, category = "") {
553
+ return (0, http_1.request)(`/api/apps/${enc(app)}/live-layer/${enc(env)}/versions/${version}`, {
554
+ query: catQuery(category)
555
+ });
556
+ }
557
+ function rollback(app, env, toVersion, category = "") {
558
+ return (0, http_1.request)(`/api/apps/${enc(app)}/live-layer/${enc(env)}/rollback`, {
559
+ method: "POST",
560
+ body: { toVersion, category }
561
+ });
562
+ }
563
+ async function listLayers2(app, env) {
564
+ const res = await (0, http_1.request)(`/api/apps/${enc(app)}/live-layer/${enc(env)}/layers`);
565
+ return res.layers || [];
566
+ }
567
+ function deleteLayer2(app, env, category) {
568
+ return (0, http_1.request)(`/api/apps/${enc(app)}/live-layer/${enc(env)}/layers`, {
569
+ method: "DELETE",
570
+ query: { category }
571
+ });
572
+ }
573
+ async function publishMutating2(app, env, category, mutate) {
574
+ let lastErr;
575
+ for (let attempt = 0; attempt < 2; attempt++) {
576
+ const cur = await getCurrent2(app, env, category);
577
+ let start = cur.config || {};
578
+ if (category && !cur.published) {
579
+ const base = await getCurrent2(app, env, "");
580
+ start = base.config || {};
581
+ }
582
+ const next = JSON.parse(JSON.stringify(start));
583
+ mutate(next);
584
+ try {
585
+ return await publish(app, env, next, cur.version || 0, category);
586
+ } catch (e) {
587
+ lastErr = e;
588
+ const status = e.status;
589
+ if (status === 409 && attempt === 0)
590
+ continue;
591
+ throw e;
592
+ }
593
+ }
594
+ throw lastErr;
595
+ }
596
+ function parseCategoryArg2(raw) {
597
+ if (!raw)
598
+ return "";
599
+ return raw.split(/[/,]/).map((s) => s.trim()).filter((s) => s.length > 0).join("/");
600
+ }
601
+ function setInPath2(obj, dotted, value) {
602
+ const parts = dotted.split(".").filter((p) => p.length > 0);
603
+ let node = obj;
604
+ for (let i = 0; i < parts.length - 1; i++) {
605
+ const k = parts[i];
606
+ if (node[k] === null || typeof node[k] !== "object" || Array.isArray(node[k]))
607
+ node[k] = {};
608
+ node = node[k];
609
+ }
610
+ node[parts[parts.length - 1]] = value;
611
+ }
612
+ function unsetInPath2(obj, dotted) {
613
+ const parts = dotted.split(".").filter((p) => p.length > 0);
614
+ let node = obj;
615
+ for (let i = 0; i < parts.length - 1; i++) {
616
+ const k = parts[i];
617
+ if (node[k] === null || typeof node[k] !== "object")
618
+ return;
619
+ node = node[k];
620
+ }
621
+ delete node[parts[parts.length - 1]];
622
+ }
623
+ function getInPath2(obj, dotted) {
624
+ const parts = dotted.split(".").filter((p) => p.length > 0);
625
+ let node = obj;
626
+ for (const k of parts) {
627
+ if (node === null || typeof node !== "object" || Array.isArray(node))
628
+ return void 0;
629
+ node = node[k];
630
+ }
631
+ return node;
632
+ }
633
+ function parseValue2(raw) {
634
+ if (raw === "true")
635
+ return true;
636
+ if (raw === "false")
637
+ return false;
638
+ if (raw === "null")
639
+ return null;
640
+ if (/^-?\d+(\.\d+)?$/.test(raw))
641
+ return Number(raw);
642
+ const t = raw.trim();
643
+ if (t.startsWith("{") || t.startsWith("[")) {
644
+ try {
645
+ return JSON.parse(t);
646
+ } catch {
647
+ }
648
+ }
649
+ return raw;
650
+ }
651
+ }
652
+ });
653
+
654
+ // ../core/dist/build.js
655
+ var require_build = __commonJS({
656
+ "../core/dist/build.js"(exports2) {
657
+ "use strict";
658
+ Object.defineProperty(exports2, "__esModule", { value: true });
659
+ exports2.isTerminalStatus = isTerminalStatus3;
660
+ exports2.listBuilds = listBuilds3;
661
+ exports2.getBuild = getBuild2;
662
+ exports2.getLogs = getLogs2;
663
+ exports2.cancelBuild = cancelBuild2;
664
+ exports2.createBuild = createBuild2;
665
+ exports2.startBuild = startBuild2;
666
+ exports2.getCredentials = getCredentials2;
667
+ exports2.uploadAndroidKeystore = uploadAndroidKeystore2;
668
+ exports2.deleteAndroidKeystore = deleteAndroidKeystore2;
669
+ exports2.uploadIosAscKey = uploadIosAscKey2;
670
+ exports2.deleteIosAscKey = deleteIosAscKey2;
671
+ exports2.setIosTeamId = setIosTeamId2;
672
+ exports2.listEnvVars = listEnvVars2;
673
+ exports2.upsertEnvVar = upsertEnvVar3;
674
+ exports2.deleteEnvVar = deleteEnvVar2;
675
+ exports2.getVersioning = getVersioning2;
676
+ exports2.setVersioning = setVersioning2;
677
+ exports2.getEasConfig = getEasConfig2;
678
+ exports2.submitBuild = submitBuild2;
679
+ exports2.listSubmissions = listSubmissions2;
680
+ exports2.getSubmission = getSubmission2;
681
+ exports2.getSubmissionLogs = getSubmissionLogs2;
682
+ exports2.cancelSubmission = cancelSubmission2;
683
+ exports2.listDevices = listDevices2;
684
+ exports2.createDevice = createDevice2;
685
+ exports2.removeDevice = removeDevice2;
686
+ exports2.getPlayCredentials = getPlayCredentials2;
687
+ exports2.uploadPlayServiceAccount = uploadPlayServiceAccount2;
688
+ exports2.deletePlayServiceAccount = deletePlayServiceAccount2;
689
+ exports2.storeAppCheck = storeAppCheck2;
690
+ exports2.createTest = createTest2;
691
+ exports2.getTest = getTest2;
692
+ exports2.listTests = listTests3;
693
+ exports2.cancelTest = cancelTest2;
694
+ var http_1 = require_http();
695
+ var enc = encodeURIComponent;
696
+ var BASE = "/build-api/v1";
697
+ var TERMINAL = ["finished", "errored", "canceled", "timed_out"];
698
+ function isTerminalStatus3(status) {
699
+ return TERMINAL.includes(status);
700
+ }
701
+ async function listBuilds3(appId) {
702
+ const res = await (0, http_1.request)(`${BASE}/apps/${enc(appId)}/builds`);
703
+ return res.builds || [];
704
+ }
705
+ async function getBuild2(id) {
706
+ return (0, http_1.request)(`${BASE}/builds/${enc(id)}`);
707
+ }
708
+ async function getLogs2(id, after = 0) {
709
+ return (0, http_1.request)(`${BASE}/builds/${enc(id)}/logs`, { query: { after } });
710
+ }
711
+ async function cancelBuild2(id) {
712
+ return (0, http_1.request)(`${BASE}/builds/${enc(id)}/cancel`, { method: "POST" });
713
+ }
714
+ async function createBuild2(appId, opts) {
715
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/builds`, {
716
+ method: "POST",
717
+ body: {
718
+ platform: opts.platform,
719
+ profile: opts.profile,
720
+ buildType: opts.buildType,
721
+ source: opts.source,
722
+ sourceObject: opts.sourceObject,
723
+ ref: opts.ref,
724
+ ...opts.saveId ? { saveId: opts.saveId } : {},
725
+ ...opts.simulator ? { simulator: true } : {},
726
+ triggerSource: "cli"
727
+ }
728
+ });
729
+ }
730
+ async function startBuild2(id) {
731
+ return (0, http_1.request)(`${BASE}/builds/${enc(id)}/start`, { method: "POST" });
732
+ }
733
+ function getCredentials2(appId) {
734
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials`);
735
+ }
736
+ function uploadAndroidKeystore2(appId, input) {
737
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials/android/keystore`, {
738
+ method: "POST",
739
+ body: input
740
+ });
741
+ }
742
+ function deleteAndroidKeystore2(appId) {
743
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials/android/keystore`, { method: "DELETE" });
744
+ }
745
+ function uploadIosAscKey2(appId, input) {
746
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials/ios/asc-api-key`, {
747
+ method: "POST",
748
+ body: input
749
+ });
750
+ }
751
+ function deleteIosAscKey2(appId) {
752
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials/ios/asc-api-key`, { method: "DELETE" });
753
+ }
754
+ function setIosTeamId2(appId, teamId) {
755
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials/ios/asc-api-key`, {
756
+ method: "PATCH",
757
+ body: { teamId }
758
+ });
759
+ }
760
+ async function listEnvVars2(appId) {
761
+ const res = await (0, http_1.request)(`${BASE}/apps/${enc(appId)}/env-vars`);
762
+ return res.envVars || [];
763
+ }
764
+ function upsertEnvVar3(appId, input) {
765
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/env-vars`, { method: "POST", body: input });
766
+ }
767
+ function deleteEnvVar2(appId, id) {
768
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/env-vars/${enc(id)}`, {
769
+ method: "DELETE"
770
+ });
771
+ }
772
+ function getVersioning2(appId, platform = "android") {
773
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/versioning`, { query: { platform } });
774
+ }
775
+ function setVersioning2(appId, input) {
776
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/versioning`, { method: "PUT", body: input });
777
+ }
778
+ function getEasConfig2(appId) {
779
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/eas-config`);
780
+ }
781
+ function submitBuild2(buildId, opts) {
782
+ return (0, http_1.request)(`${BASE}/builds/${enc(buildId)}/submit`, {
783
+ method: "POST",
784
+ body: {
785
+ mode: opts.mode,
786
+ ...opts.destination ? { destination: opts.destination } : {},
787
+ ...opts.rollout != null ? { rollout: opts.rollout } : {},
788
+ triggerSource: "cli"
789
+ }
790
+ });
791
+ }
792
+ async function listSubmissions2(appId) {
793
+ const res = await (0, http_1.request)(`${BASE}/apps/${enc(appId)}/submissions`);
794
+ return res.submissions || [];
795
+ }
796
+ function getSubmission2(id) {
797
+ return (0, http_1.request)(`${BASE}/submissions/${enc(id)}`);
798
+ }
799
+ function getSubmissionLogs2(id, after) {
800
+ return (0, http_1.request)(`${BASE}/submissions/${enc(id)}/logs`, {
801
+ query: after ? { after } : void 0
802
+ });
803
+ }
804
+ function cancelSubmission2(id) {
805
+ return (0, http_1.request)(`${BASE}/submissions/${enc(id)}/cancel`, { method: "POST" });
806
+ }
807
+ async function listDevices2(appId) {
808
+ const res = await (0, http_1.request)(`${BASE}/apps/${enc(appId)}/devices`);
809
+ return res.devices || [];
810
+ }
811
+ function createDevice2(appId, input) {
812
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/devices`, { method: "POST", body: input });
813
+ }
814
+ function removeDevice2(appId, id) {
815
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/devices/${enc(id)}`, { method: "DELETE" });
816
+ }
817
+ async function getPlayCredentials2(appId) {
818
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials/play`);
819
+ }
820
+ function uploadPlayServiceAccount2(appId, input) {
821
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials/android/play-service-account`, { method: "POST", body: input });
822
+ }
823
+ function deletePlayServiceAccount2(appId) {
824
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/credentials/android/play-service-account`, { method: "DELETE" });
825
+ }
826
+ function storeAppCheck2(appId, platform, identifier) {
827
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/store-app-check`, {
828
+ query: { platform, identifier }
829
+ });
830
+ }
831
+ function createTest2(appId, opts) {
832
+ return (0, http_1.request)(`${BASE}/apps/${enc(appId)}/tests`, {
833
+ method: "POST",
834
+ body: {
835
+ buildId: opts.buildId,
836
+ platform: opts.platform ?? "android",
837
+ triggerSource: opts.triggerSource ?? "cli"
838
+ }
839
+ });
840
+ }
841
+ function getTest2(id) {
842
+ return (0, http_1.request)(`${BASE}/tests/${enc(id)}`);
843
+ }
844
+ async function listTests3(appId) {
845
+ const res = await (0, http_1.request)(`${BASE}/apps/${enc(appId)}/tests`);
846
+ return res.tests || [];
847
+ }
848
+ function cancelTest2(id) {
849
+ return (0, http_1.request)(`${BASE}/tests/${enc(id)}/cancel`, { method: "POST" });
850
+ }
851
+ }
852
+ });
853
+
854
+ // ../core/dist/app.js
855
+ var require_app = __commonJS({
856
+ "../core/dist/app.js"(exports2) {
857
+ "use strict";
858
+ Object.defineProperty(exports2, "__esModule", { value: true });
859
+ exports2.createApp = createApp3;
860
+ exports2.listApps = listApps2;
861
+ exports2.getApp = getApp2;
862
+ exports2.deleteApp = deleteApp2;
863
+ exports2.listBlueprints = listBlueprints2;
864
+ var http_1 = require_http();
865
+ var enc = encodeURIComponent;
866
+ async function createApp3(name, blueprint) {
867
+ return (0, http_1.request)("/api/apps", {
868
+ method: "POST",
869
+ body: { name, ...blueprint ? { blueprint } : {} }
870
+ });
871
+ }
872
+ async function listApps2() {
873
+ const res = await (0, http_1.request)("/api/apps");
874
+ return res.apps || [];
875
+ }
876
+ async function getApp2(id) {
877
+ const res = await (0, http_1.request)(`/api/apps/${enc(id)}`);
878
+ return res.app;
879
+ }
880
+ async function deleteApp2(id) {
881
+ return (0, http_1.request)(`/api/apps/${enc(id)}`, { method: "DELETE" });
882
+ }
883
+ async function listBlueprints2() {
884
+ const res = await (0, http_1.request)("/api/blueprints");
885
+ return res.blueprints || [];
886
+ }
887
+ }
888
+ });
889
+
890
+ // ../core/dist/source.js
891
+ var require_source = __commonJS({
892
+ "../core/dist/source.js"(exports2) {
893
+ "use strict";
894
+ Object.defineProperty(exports2, "__esModule", { value: true });
895
+ exports2.getFiles = getFiles2;
896
+ exports2.getSaveFileContents = getSaveFileContents;
897
+ exports2.getSaveFiles = getSaveFiles2;
898
+ exports2.getCurrentFiles = getCurrentFiles2;
899
+ exports2.listSaves = listSaves2;
900
+ exports2.getSaveManifest = getSaveManifest2;
901
+ exports2.getLatestSaveMeta = getLatestSaveMeta2;
902
+ exports2.pushSave = pushSave2;
903
+ var http_1 = require_http();
904
+ var enc = encodeURIComponent;
905
+ async function getFiles2(appId) {
906
+ const res = await (0, http_1.request)(`/api/apps/${enc(appId)}/files`);
907
+ return res.files || {};
908
+ }
909
+ async function getSaveFileContents(appId, saveId, filePath) {
910
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/saves/${enc(saveId)}/file`, {
911
+ query: { path: filePath },
912
+ raw: true
913
+ });
914
+ }
915
+ async function getSaveFiles2(appId, saveId) {
916
+ const manifest = await getSaveManifest2(appId, saveId);
917
+ const entries = manifest?.files ?? [];
918
+ const map = {};
919
+ let cursor = 0;
920
+ const worker = async () => {
921
+ for (; ; ) {
922
+ const i = cursor++;
923
+ if (i >= entries.length)
924
+ return;
925
+ const f = entries[i];
926
+ const path11 = f.path ?? f.filePath;
927
+ if (!path11)
928
+ continue;
929
+ const contents = await getSaveFileContents(appId, saveId, path11);
930
+ map[path11] = { contents, type: f.type ?? "CODE" };
931
+ }
932
+ };
933
+ const workerCount = Math.min(8, entries.length);
934
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
935
+ return map;
936
+ }
937
+ async function getCurrentFiles2(appId) {
938
+ const saves = await listSaves2(appId);
939
+ if (!saves.length)
940
+ return getFiles2(appId);
941
+ return getSaveFiles2(appId, saves[0]);
942
+ }
943
+ async function listSaves2(appId) {
944
+ const res = await (0, http_1.request)(`/api/apps/${enc(appId)}/saves`);
945
+ return res.saves || [];
946
+ }
947
+ async function getSaveManifest2(appId, saveId) {
948
+ const res = await (0, http_1.request)(`/api/apps/${enc(appId)}/saves/${enc(saveId)}`);
949
+ return res.manifest || null;
950
+ }
951
+ async function getLatestSaveMeta2(appId) {
952
+ const saves = await listSaves2(appId);
953
+ if (!saves.length)
954
+ return null;
955
+ const m = await getSaveManifest2(appId, saves[0]);
956
+ if (!m)
957
+ return null;
958
+ return { sdkVersion: m.sdkVersion, dependencies: m.dependencies };
959
+ }
960
+ async function pushSave2(appId, input) {
961
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/saves`, {
962
+ method: "POST",
963
+ body: {
964
+ files: input.files,
965
+ dependencies: input.dependencies ?? {},
966
+ sdkVersion: input.sdkVersion ?? "54.0.0",
967
+ branch: input.branch ?? null
968
+ }
969
+ });
970
+ }
971
+ }
972
+ });
973
+
974
+ // ../core/dist/assets.js
975
+ var require_assets = __commonJS({
976
+ "../core/dist/assets.js"(exports2) {
977
+ "use strict";
978
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
979
+ if (k2 === void 0) k2 = k;
980
+ var desc = Object.getOwnPropertyDescriptor(m, k);
981
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
982
+ desc = { enumerable: true, get: function() {
983
+ return m[k];
984
+ } };
985
+ }
986
+ Object.defineProperty(o, k2, desc);
987
+ }) : (function(o, m, k, k2) {
988
+ if (k2 === void 0) k2 = k;
989
+ o[k2] = m[k];
990
+ }));
991
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
992
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
993
+ }) : function(o, v) {
994
+ o["default"] = v;
995
+ });
996
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
997
+ var ownKeys = function(o) {
998
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
999
+ var ar = [];
1000
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1001
+ return ar;
1002
+ };
1003
+ return ownKeys(o);
1004
+ };
1005
+ return function(mod) {
1006
+ if (mod && mod.__esModule) return mod;
1007
+ var result = {};
1008
+ if (mod != null) {
1009
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1010
+ }
1011
+ __setModuleDefault(result, mod);
1012
+ return result;
1013
+ };
1014
+ })();
1015
+ Object.defineProperty(exports2, "__esModule", { value: true });
1016
+ exports2.uploadAsset = uploadAsset2;
1017
+ exports2.listAssets = listAssets2;
1018
+ exports2.deleteAsset = deleteAsset2;
1019
+ var fs8 = __importStar(require("fs"));
1020
+ var path11 = __importStar(require("path"));
1021
+ var errors_1 = require_errors();
1022
+ var output_1 = require_output();
1023
+ var auth_1 = require_auth();
1024
+ var http_1 = require_http();
1025
+ var MIME = {
1026
+ ".png": "image/png",
1027
+ ".jpg": "image/jpeg",
1028
+ ".jpeg": "image/jpeg",
1029
+ ".gif": "image/gif",
1030
+ ".svg": "image/svg+xml",
1031
+ ".webp": "image/webp",
1032
+ ".mp4": "video/mp4",
1033
+ ".webm": "video/webm",
1034
+ ".mov": "video/quicktime",
1035
+ ".ttf": "font/ttf",
1036
+ ".otf": "font/otf",
1037
+ ".woff": "font/woff",
1038
+ ".woff2": "font/woff2"
1039
+ };
1040
+ function mimeFor(file) {
1041
+ return MIME[path11.extname(file).toLowerCase()] || "application/octet-stream";
1042
+ }
1043
+ var enc = encodeURIComponent;
1044
+ function authHeaders() {
1045
+ const token = (0, auth_1.getToken)();
1046
+ if (!token)
1047
+ return {};
1048
+ return { Cookie: `tile_session=${token}`, Authorization: `Bearer ${token}` };
1049
+ }
1050
+ async function uploadAsset2(appId, filePath) {
1051
+ let data;
1052
+ try {
1053
+ data = await fs8.promises.readFile(filePath);
1054
+ } catch (e) {
1055
+ throw new errors_1.TileError(`Could not read file "${filePath}" (${e.message}).`, output_1.ExitCode.USAGE, "Pass a path to an existing local file.");
1056
+ }
1057
+ const name = path11.basename(filePath);
1058
+ const form = new FormData();
1059
+ form.append("file", new Blob([data], { type: mimeFor(name) }), name);
1060
+ const url = `${(0, http_1.baseUrl)()}/api/apps/${enc(appId)}/assets`;
1061
+ let res;
1062
+ try {
1063
+ res = await fetch(url, { method: "POST", headers: authHeaders(), body: form });
1064
+ } catch (e) {
1065
+ throw new errors_1.TileError(`Could not reach the tile backend at ${(0, http_1.baseUrl)()}. Is the gateway up? (${e.message})`, output_1.ExitCode.NETWORK, "Start the dev stack, or set TILE_API_URL to the right host.");
1066
+ }
1067
+ const text = await res.text();
1068
+ let parsed;
1069
+ try {
1070
+ parsed = text ? JSON.parse(text) : void 0;
1071
+ } catch {
1072
+ parsed = text;
1073
+ }
1074
+ if (!res.ok) {
1075
+ const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : typeof parsed === "string" && parsed ? parsed : res.statusText;
1076
+ throw new errors_1.TileError(`${res.status} ${msg}`, (0, errors_1.exitCodeForStatus)(res.status), res.status === 401 || res.status === 403 ? "Run `tile login` (or set TILE_TOKEN)." : void 0);
1077
+ }
1078
+ const asset = parsed;
1079
+ return { ...asset, absoluteUrl: (0, http_1.baseUrl)() + asset.url };
1080
+ }
1081
+ async function listAssets2(appId) {
1082
+ const url = `${(0, http_1.baseUrl)()}/api/apps/${enc(appId)}/assets`;
1083
+ const res = await fetch(url, { headers: { Accept: "application/json", ...authHeaders() } });
1084
+ const body = await res.json().catch(() => ({}));
1085
+ if (!res.ok) {
1086
+ throw new errors_1.TileError(`${res.status} ${body.error || res.statusText}`, (0, errors_1.exitCodeForStatus)(res.status));
1087
+ }
1088
+ return body.assets || [];
1089
+ }
1090
+ async function deleteAsset2(appId, assetId) {
1091
+ const url = `${(0, http_1.baseUrl)()}/api/apps/${enc(appId)}/assets/${enc(assetId)}`;
1092
+ const res = await fetch(url, { method: "DELETE", headers: { Accept: "application/json", ...authHeaders() } });
1093
+ if (!res.ok) {
1094
+ const body = await res.json().catch(() => ({}));
1095
+ throw new errors_1.TileError(`${res.status} ${body.error || res.statusText}`, (0, errors_1.exitCodeForStatus)(res.status));
1096
+ }
1097
+ }
1098
+ }
1099
+ });
1100
+
1101
+ // ../core/dist/github.js
1102
+ var require_github = __commonJS({
1103
+ "../core/dist/github.js"(exports2) {
1104
+ "use strict";
1105
+ Object.defineProperty(exports2, "__esModule", { value: true });
1106
+ exports2.getGithubStatus = getGithubStatus2;
1107
+ exports2.getInstallUrl = getInstallUrl2;
1108
+ exports2.disconnectGithub = disconnectGithub2;
1109
+ exports2.listUserRepos = listUserRepos2;
1110
+ exports2.connectRepo = connectRepo2;
1111
+ exports2.githubPush = githubPush2;
1112
+ exports2.getBranchDrift = getBranchDrift2;
1113
+ exports2.materializeBranch = materializeBranch2;
1114
+ var http_1 = require_http();
1115
+ var enc = encodeURIComponent;
1116
+ function getGithubStatus2(appId) {
1117
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/github`);
1118
+ }
1119
+ async function getInstallUrl2(appId, returnTo) {
1120
+ const res = await (0, http_1.request)(`/api/github/install-url`, {
1121
+ query: { appId, ...returnTo ? { returnTo } : {} }
1122
+ });
1123
+ return res.url;
1124
+ }
1125
+ function disconnectGithub2(appId) {
1126
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/github/disconnect`, {
1127
+ method: "POST"
1128
+ });
1129
+ }
1130
+ async function listUserRepos2(appId, page = 1, perPage = 30) {
1131
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/github/user-repos`, { query: { page, perPage } });
1132
+ }
1133
+ function connectRepo2(appId, owner, repo) {
1134
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/github/connect-repo`, { method: "POST", body: { owner, repo } });
1135
+ }
1136
+ function githubPush2(appId, opts = {}) {
1137
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/github/push`, {
1138
+ method: "POST",
1139
+ body: { branch: opts.branch, message: opts.message }
1140
+ });
1141
+ }
1142
+ function getBranchDrift2(appId, branch) {
1143
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/branches/${enc(branch)}/drift`);
1144
+ }
1145
+ function materializeBranch2(appId, branch) {
1146
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/branches/${enc(branch)}/materialize`, { method: "POST" });
1147
+ }
1148
+ }
1149
+ });
1150
+
1151
+ // ../core/dist/integrations.js
1152
+ var require_integrations = __commonJS({
1153
+ "../core/dist/integrations.js"(exports2) {
1154
+ "use strict";
1155
+ Object.defineProperty(exports2, "__esModule", { value: true });
1156
+ exports2.listIntegrations = listIntegrations2;
1157
+ exports2.connectIntegration = connectIntegration2;
1158
+ exports2.disconnectIntegration = disconnectIntegration2;
1159
+ exports2.resolveIntegration = resolveIntegration2;
1160
+ var http_1 = require_http();
1161
+ var enc = encodeURIComponent;
1162
+ function listIntegrations2(appId) {
1163
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/integrations`);
1164
+ }
1165
+ function connectIntegration2(appId, provider, body) {
1166
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/integrations/${enc(provider)}/connect`, {
1167
+ method: "POST",
1168
+ body
1169
+ });
1170
+ }
1171
+ function disconnectIntegration2(appId, provider, category) {
1172
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/integrations/${enc(provider)}`, {
1173
+ method: "DELETE",
1174
+ query: category !== void 0 ? { category } : void 0
1175
+ });
1176
+ }
1177
+ function resolveIntegration2(appId, provider, category) {
1178
+ return (0, http_1.request)(`/api/apps/${enc(appId)}/integrations/${enc(provider)}/resolve`, {
1179
+ query: category !== void 0 ? { category } : void 0
1180
+ });
1181
+ }
1182
+ }
1183
+ });
1184
+
1185
+ // ../core/dist/index.js
1186
+ var require_dist = __commonJS({
1187
+ "../core/dist/index.js"(exports2) {
1188
+ "use strict";
1189
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
1190
+ if (k2 === void 0) k2 = k;
1191
+ var desc = Object.getOwnPropertyDescriptor(m, k);
1192
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1193
+ desc = { enumerable: true, get: function() {
1194
+ return m[k];
1195
+ } };
1196
+ }
1197
+ Object.defineProperty(o, k2, desc);
1198
+ }) : (function(o, m, k, k2) {
1199
+ if (k2 === void 0) k2 = k;
1200
+ o[k2] = m[k];
1201
+ }));
1202
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
1203
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
1204
+ };
1205
+ Object.defineProperty(exports2, "__esModule", { value: true });
1206
+ __exportStar(require_output(), exports2);
1207
+ __exportStar(require_errors(), exports2);
1208
+ __exportStar(require_http(), exports2);
1209
+ __exportStar(require_auth(), exports2);
1210
+ __exportStar(require_config(), exports2);
1211
+ __exportStar(require_liveLayer(), exports2);
1212
+ __exportStar(require_build(), exports2);
1213
+ __exportStar(require_app(), exports2);
1214
+ __exportStar(require_source(), exports2);
1215
+ __exportStar(require_assets(), exports2);
1216
+ __exportStar(require_github(), exports2);
1217
+ __exportStar(require_integrations(), exports2);
1218
+ }
1219
+ });
1220
+
1221
+ // package.json
1222
+ var require_package = __commonJS({
1223
+ "package.json"(exports2, module2) {
1224
+ module2.exports = {
1225
+ name: "@apptile/tile-cli",
1226
+ version: "0.1.0-beta.0",
1227
+ description: "Tile \u2014 the unified, agent-friendly CLI for Tile apps (source, live-layer, builds, OTA).",
1228
+ license: "UNLICENSED",
1229
+ repository: {
1230
+ type: "git",
1231
+ url: "git+https://github.com/clearsight-dev/tile-cli-v2.git",
1232
+ directory: "packages/cli"
1233
+ },
1234
+ engines: {
1235
+ node: ">=18"
1236
+ },
1237
+ bin: {
1238
+ tile: "dist/index.js"
1239
+ },
1240
+ main: "dist/index.js",
1241
+ scripts: {
1242
+ build: "tsup",
1243
+ typecheck: "tsc -p tsconfig.json --noEmit",
1244
+ prepublishOnly: "npm --prefix ../core run build && npm run typecheck && npm run build"
1245
+ },
1246
+ dependencies: {
1247
+ commander: "^12.1.0",
1248
+ qrcode: "^1.5.4",
1249
+ "qrcode-terminal": "^0.12.0",
1250
+ "socket.io": "^4.8.3"
1251
+ },
1252
+ devDependencies: {
1253
+ "@tile/core": "0.1.0",
1254
+ "@types/qrcode": "^1.5.5",
1255
+ tsup: "^8.3.5"
1256
+ },
1257
+ files: [
1258
+ "dist",
1259
+ "README.md"
1260
+ ],
1261
+ publishConfig: {
1262
+ access: "public"
1263
+ }
1264
+ };
1265
+ }
1266
+ });
1267
+
1268
+ // src/index.ts
1269
+ var import_commander2 = require("commander");
1270
+ var import_core12 = __toESM(require_dist());
1271
+
1272
+ // src/commands/auth.ts
1273
+ var readline = __toESM(require("readline"));
1274
+ var import_stream = require("stream");
1275
+ var import_core = __toESM(require_dist());
1276
+ function prompt(question, mask = false) {
1277
+ if (!process.stdin.isTTY) {
1278
+ throw new import_core.TileError(
1279
+ "Cannot prompt for input: this is not an interactive terminal.",
1280
+ import_core.ExitCode.USAGE,
1281
+ "Pass the values as flags (--email/--password, plus --name for register), or set TILE_TOKEN."
1282
+ );
1283
+ }
1284
+ return new Promise((resolve7) => {
1285
+ let muted = false;
1286
+ const mutableOut = new import_stream.Writable({
1287
+ write(chunk, _enc, cb) {
1288
+ if (!muted) process.stderr.write(chunk);
1289
+ cb();
1290
+ }
1291
+ });
1292
+ const rl = readline.createInterface({ input: process.stdin, output: mutableOut, terminal: true });
1293
+ process.stderr.write(question);
1294
+ if (mask) muted = true;
1295
+ rl.question("", (answer) => {
1296
+ if (mask) process.stderr.write("\n");
1297
+ rl.close();
1298
+ resolve7(answer.trim());
1299
+ });
1300
+ });
1301
+ }
1302
+ function registerAuthCommands(program2) {
1303
+ program2.command("login").description("Authenticate with your Tile email + password and store a session token.").argument("[email]", "account email (you can also pass --email, or be prompted)").option("-e, --email <email>", "account email").option("-p, --password <password>", "account password (omit to be prompted)").action(async (emailArg, opts) => {
1304
+ const email = opts.email || emailArg;
1305
+ if ((!email || !opts.password) && process.stdin.isTTY) {
1306
+ (0, import_core.info)("Log in to Tile. Enter the email and password for your account.");
1307
+ }
1308
+ const resolvedEmail = email || await prompt("Email: ");
1309
+ const password = opts.password || await prompt("Password (typing is hidden): ", true);
1310
+ const creds = await (0, import_core.login)(resolvedEmail, password);
1311
+ if ((0, import_core.isJsonMode)()) {
1312
+ (0, import_core.out)({ user: creds.user });
1313
+ } else {
1314
+ (0, import_core.info)(`Logged in as ${creds.user?.email || resolvedEmail}.`);
1315
+ (0, import_core.hint)("Next: `tile whoami` to confirm, or `tile live-layer get` to read flags.");
1316
+ }
1317
+ });
1318
+ program2.command("register").description("Create a new Tile account with email + password, then log in.").argument("[email]", "account email (you can also pass --email, or be prompted)").option("-e, --email <email>", "account email").option("-p, --password <password>", "account password (min 8 chars; omit to be prompted)").option("-n, --name <name>", "your name").action(
1319
+ async (emailArg, opts) => {
1320
+ const email = opts.email || emailArg;
1321
+ if ((!email || !opts.name || !opts.password) && process.stdin.isTTY) {
1322
+ (0, import_core.info)("Create a Tile account. Enter your email, name, and a password (min 8 chars).");
1323
+ }
1324
+ const resolvedEmail = email || await prompt("Email: ");
1325
+ const name = opts.name || await prompt("Name: ");
1326
+ const password = opts.password || await prompt("Password (min 8 chars, typing is hidden): ", true);
1327
+ const creds = await (0, import_core.register)(resolvedEmail, password, name);
1328
+ if ((0, import_core.isJsonMode)()) {
1329
+ (0, import_core.out)({ user: creds.user });
1330
+ } else {
1331
+ (0, import_core.info)(`Account created. Logged in as ${creds.user?.email || resolvedEmail}.`);
1332
+ (0, import_core.hint)('Next: `tile whoami` to confirm, or `tile init --name "My App"` to start a project.');
1333
+ }
1334
+ }
1335
+ );
1336
+ program2.command("logout").description("Remove the stored session token.").action(() => {
1337
+ (0, import_core.logout)();
1338
+ if ((0, import_core.isJsonMode)()) (0, import_core.out)({ ok: true });
1339
+ else (0, import_core.info)("Logged out.");
1340
+ });
1341
+ program2.command("whoami").description("Show the currently authenticated user.").action(async () => {
1342
+ const user = await (0, import_core.whoami)();
1343
+ if ((0, import_core.isJsonMode)()) (0, import_core.out)({ user });
1344
+ else (0, import_core.info)(`${user.email}${user.name ? ` (${user.name})` : ""} \u2014 id ${user.id}`);
1345
+ });
1346
+ }
1347
+
1348
+ // src/commands/live-layer.ts
1349
+ var fs = __toESM(require("fs"));
1350
+ var path = __toESM(require("path"));
1351
+ var import_core2 = __toESM(require_dist());
1352
+ var DEFAULT_PUBLIC_BASE = "https://storage.googleapis.com/tile-livelayer-configs";
1353
+ function target(opts) {
1354
+ return { app: (0, import_core2.resolveApp)(opts), env: (0, import_core2.resolveEnv)(opts), category: (0, import_core2.parseCategoryArg)(opts.category) };
1355
+ }
1356
+ function catLabel(category) {
1357
+ return category || "(base)";
1358
+ }
1359
+ async function readStdin() {
1360
+ const chunks = [];
1361
+ for await (const c of process.stdin) chunks.push(c);
1362
+ return Buffer.concat(chunks).toString("utf8");
1363
+ }
1364
+ function registerLiveLayerCommands(program2) {
1365
+ const ll = program2.command("live-layer", { hidden: true }).alias("ll").description("Manage remote nested config, sliced by category, for an app + environment.");
1366
+ const LIVE_LAYER_WRITES_ENABLED = false;
1367
+ const assertWritable = () => {
1368
+ if (!LIVE_LAYER_WRITES_ENABLED)
1369
+ throw new import_core2.TileError(
1370
+ "Live Layer publishing is disabled.",
1371
+ import_core2.ExitCode.USAGE,
1372
+ "Remote-config writes are turned off in this CLI."
1373
+ );
1374
+ };
1375
+ const withTarget = (cmd) => cmd.option("--app <appId>", "app id (or tile.json / TILE_APP)").option("--env <env>", "environment, e.g. preview/dev/prod (or tile.json / TILE_ENV)").option("--category <key>", "category, e.g. in/mumbai (omit for the base layer)");
1376
+ withTarget(
1377
+ ll.command("get [path]").description("Print the config, or one dotted path (e.g. home.cta.label), for a category.")
1378
+ ).action(async (dpath, opts) => {
1379
+ const { app, env, category } = target(opts);
1380
+ const cur = await (0, import_core2.getCurrent)(app, env, category);
1381
+ const config = cur.config || {};
1382
+ if (dpath) {
1383
+ const v = (0, import_core2.getInPath)(config, dpath);
1384
+ if (v === void 0) throw new import_core2.TileError(`Path "${dpath}" is not set.`, import_core2.ExitCode.NOT_FOUND);
1385
+ (0, import_core2.out)(v);
1386
+ return;
1387
+ }
1388
+ if ((0, import_core2.isJsonMode)()) (0, import_core2.out)(cur);
1389
+ else (0, import_core2.out)(config);
1390
+ (0, import_core2.hint)(`(${catLabel(category)} v${cur.version}) Next: \`tile live-layer set <path>=<value>\` or \`tile live-layer versions\`.`);
1391
+ });
1392
+ withTarget(
1393
+ ll.command("set <pairs...>").description("Set one or more dotted-path=value entries (types auto-inferred) and publish.")
1394
+ ).action(async (pairs, opts) => {
1395
+ assertWritable();
1396
+ const { app, env, category } = target(opts);
1397
+ const patch = [];
1398
+ for (const p of pairs) {
1399
+ const i = p.indexOf("=");
1400
+ if (i < 0) throw new import_core2.TileError(`Bad pair "${p}" \u2014 expected path=value.`, import_core2.ExitCode.USAGE);
1401
+ patch.push({ path: p.slice(0, i), value: (0, import_core2.parseValue)(p.slice(i + 1)) });
1402
+ }
1403
+ const res = await (0, import_core2.publishMutating)(
1404
+ app,
1405
+ env,
1406
+ category,
1407
+ (c) => patch.forEach(({ path: pp, value }) => (0, import_core2.setInPath)(c, pp, value))
1408
+ );
1409
+ if ((0, import_core2.isJsonMode)()) (0, import_core2.out)(res);
1410
+ else (0, import_core2.info)(`\u2713 Published ${catLabel(category)} v${res.version} (${patch.length} change(s)).`);
1411
+ (0, import_core2.hint)(
1412
+ `Next: \`tile live-layer get --category ${category || ""}\` to verify, or \`tile live-layer rollback --to ${Math.max(res.version - 1, 1)}\` to undo.`
1413
+ );
1414
+ });
1415
+ withTarget(
1416
+ ll.command("rm <paths...>").description("Remove one or more dotted paths and publish.")
1417
+ ).action(async (paths, opts) => {
1418
+ assertWritable();
1419
+ const { app, env, category } = target(opts);
1420
+ const res = await (0, import_core2.publishMutating)(
1421
+ app,
1422
+ env,
1423
+ category,
1424
+ (c) => paths.forEach((p) => (0, import_core2.unsetInPath)(c, p))
1425
+ );
1426
+ if ((0, import_core2.isJsonMode)()) (0, import_core2.out)(res);
1427
+ else (0, import_core2.info)(`\u2713 Published ${catLabel(category)} v${res.version} (removed ${paths.length}).`);
1428
+ (0, import_core2.hint)(`Next: \`tile live-layer get\` to verify.`);
1429
+ });
1430
+ withTarget(
1431
+ ll.command("publish").description("Replace the ENTIRE config for a category from a JSON file (or --stdin) and publish.").option("--file <path>", "path to a JSON config object").option("--stdin", "read the JSON object from stdin")
1432
+ ).action(async (opts) => {
1433
+ assertWritable();
1434
+ const { app, env, category } = target(opts);
1435
+ let raw;
1436
+ if (opts.file) raw = fs.readFileSync(opts.file, "utf8");
1437
+ else if (opts.stdin) raw = await readStdin();
1438
+ else throw new import_core2.TileError("Provide --file <path> or --stdin.", import_core2.ExitCode.USAGE);
1439
+ let content;
1440
+ try {
1441
+ content = JSON.parse(raw);
1442
+ } catch (e) {
1443
+ throw new import_core2.TileError(`Input is not valid JSON: ${e.message}`, import_core2.ExitCode.USAGE);
1444
+ }
1445
+ if (content === null || typeof content !== "object" || Array.isArray(content)) {
1446
+ throw new import_core2.TileError("Input must be a JSON config object.", import_core2.ExitCode.USAGE);
1447
+ }
1448
+ const obj = content;
1449
+ const res = await (0, import_core2.publishMutating)(app, env, category, (c) => {
1450
+ Object.keys(c).forEach((k) => delete c[k]);
1451
+ Object.assign(c, obj);
1452
+ });
1453
+ if ((0, import_core2.isJsonMode)()) (0, import_core2.out)(res);
1454
+ else (0, import_core2.info)(`\u2713 Published ${catLabel(category)} v${res.version} (${Object.keys(res.config).length} top-level keys).`);
1455
+ (0, import_core2.hint)(`Next: \`tile live-layer versions\` to see history.`);
1456
+ });
1457
+ withTarget(
1458
+ ll.command("layers").description("List published categories for this app + environment.")
1459
+ ).action(async (opts) => {
1460
+ const { app, env } = target(opts);
1461
+ const layers = await (0, import_core2.listLayers)(app, env);
1462
+ if ((0, import_core2.isJsonMode)()) {
1463
+ (0, import_core2.out)(layers);
1464
+ return;
1465
+ }
1466
+ if (!layers.length) {
1467
+ (0, import_core2.info)("No categories published yet.");
1468
+ } else {
1469
+ for (const l of layers) {
1470
+ (0, import_core2.info)(`${catLabel(l.category)} v${l.version} ${l.updatedBy ?? "-"} ${l.updatedAt ?? "-"}`);
1471
+ }
1472
+ }
1473
+ (0, import_core2.hint)(`Next: \`tile live-layer get --category <key>\` or \`tile live-layer rm-layer <key>\`.`);
1474
+ });
1475
+ withTarget(
1476
+ ll.command("rm-layer <category>").description("Delete a category entirely (its history, manifest entry, and blobs).")
1477
+ ).action(async (categoryArg, opts) => {
1478
+ assertWritable();
1479
+ const { app, env } = target(opts);
1480
+ const category = (0, import_core2.parseCategoryArg)(categoryArg);
1481
+ if (!category) throw new import_core2.TileError("Refusing to delete the base layer.", import_core2.ExitCode.USAGE);
1482
+ const res = await (0, import_core2.deleteLayer)(app, env, category);
1483
+ if ((0, import_core2.isJsonMode)()) (0, import_core2.out)(res);
1484
+ else (0, import_core2.info)(`\u2713 Deleted category ${category}.`);
1485
+ (0, import_core2.hint)(`Next: \`tile live-layer layers\` to confirm.`);
1486
+ });
1487
+ withTarget(
1488
+ ll.command("versions").description("List published versions for a category (newest first).")
1489
+ ).action(async (opts) => {
1490
+ const { app, env, category } = target(opts);
1491
+ const versions = await (0, import_core2.listVersions)(app, env, category);
1492
+ if ((0, import_core2.isJsonMode)()) {
1493
+ (0, import_core2.out)(versions);
1494
+ return;
1495
+ }
1496
+ if (!versions.length) {
1497
+ (0, import_core2.info)(`No versions published for ${catLabel(category)}.`);
1498
+ } else {
1499
+ for (const v of versions) {
1500
+ (0, import_core2.info)(`v${v.version} ${v.createdBy ?? "-"} ${v.createdAt ?? "-"}${v.note ? " " + v.note : ""}`);
1501
+ }
1502
+ }
1503
+ (0, import_core2.hint)(`Next: \`tile live-layer rollback --to <n>\` to revert.`);
1504
+ });
1505
+ withTarget(
1506
+ ll.command("rollback").description("Re-publish a previous version of a category as the new current.").requiredOption("--to <version>", "the version number to roll back to")
1507
+ ).action(async (opts) => {
1508
+ assertWritable();
1509
+ const { app, env, category } = target(opts);
1510
+ const to = parseInt(opts.to, 10);
1511
+ if (!to) throw new import_core2.TileError("--to must be a version number.", import_core2.ExitCode.USAGE);
1512
+ const res = await (0, import_core2.rollback)(app, env, to, category);
1513
+ if ((0, import_core2.isJsonMode)()) (0, import_core2.out)(res);
1514
+ else (0, import_core2.info)(`\u2713 Rolled back ${catLabel(category)} to v${to} \u2192 published v${res.version}.`);
1515
+ (0, import_core2.hint)(`Next: \`tile live-layer get\` to verify.`);
1516
+ });
1517
+ ll.command("pull").description("Public read of the published config (base by default) \u2192 write a bootstrap snapshot.").option("--app <appId>", "app id (or TILE_APP)").option("--env <env>", "environment (or TILE_ENV)").option("--category <key>", "pull a specific category instead of the base layer").option("--out <file>", "output path", "assets/tile-live-layer.json").option("--base-url <url>", "public storage base URL", DEFAULT_PUBLIC_BASE).action(async (opts) => {
1518
+ const app = (0, import_core2.resolveApp)(opts);
1519
+ const env = (0, import_core2.resolveEnv)(opts);
1520
+ const category = (0, import_core2.parseCategoryArg)(opts.category);
1521
+ const baseUrl = opts.baseUrl.replace(/\/+$/, "");
1522
+ const manifestUrl = `${baseUrl}/live-layer/${app}/${env}/manifest.json`;
1523
+ (0, import_core2.info)(`Pulling ${app}/${env} [${catLabel(category)}] from ${manifestUrl}`);
1524
+ let config = {};
1525
+ let version = 0;
1526
+ try {
1527
+ const mRes = await fetch(manifestUrl, { headers: { "Cache-Control": "no-cache" } });
1528
+ if (!mRes.ok) throw new Error(`HTTP ${mRes.status}`);
1529
+ const manifest = await mRes.json();
1530
+ const layers = manifest.layers || {};
1531
+ const layer = layers[category] || layers[""];
1532
+ if (!layer) throw new Error("no base or matching category in manifest");
1533
+ version = layer.version;
1534
+ const bRes = await fetch(`${baseUrl}/${layer.blobKey}`);
1535
+ if (!bRes.ok) throw new Error(`HTTP ${bRes.status}`);
1536
+ const blob = await bRes.json();
1537
+ config = blob.config || {};
1538
+ } catch (e) {
1539
+ (0, import_core2.info)(` ! ${e.message} \u2014 writing empty bootstrap snapshot.`);
1540
+ }
1541
+ const outPath = path.resolve(process.cwd(), opts.out);
1542
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
1543
+ fs.writeFileSync(outPath, JSON.stringify(config, null, 2) + "\n");
1544
+ if ((0, import_core2.isJsonMode)()) (0, import_core2.out)({ version, keys: Object.keys(config).length, file: opts.out });
1545
+ else (0, import_core2.info)(` \u2713 wrote ${Object.keys(config).length} top-level key(s) (v${version}) \u2192 ${opts.out}`);
1546
+ });
1547
+ }
1548
+
1549
+ // src/commands/build.ts
1550
+ var fs2 = __toESM(require("fs"));
1551
+ var os = __toESM(require("os"));
1552
+ var path2 = __toESM(require("path"));
1553
+ var import_child_process = require("child_process");
1554
+ var import_commander = require("commander");
1555
+
1556
+ // src/mode.ts
1557
+ function isAgentMode() {
1558
+ return process.env.TILE_AGENT === "1";
1559
+ }
1560
+ function blockedInAgentMode(command, instead) {
1561
+ if (!isAgentMode()) return false;
1562
+ console.error(
1563
+ `\`tile ${command}\` is for local developer use and is not available in the Tile AI sandbox.
1564
+ ${instead}`
1565
+ );
1566
+ process.exitCode = 2;
1567
+ return true;
1568
+ }
1569
+
1570
+ // src/commands/build.ts
1571
+ var import_core3 = __toESM(require_dist());
1572
+
1573
+ // src/lib/qr.ts
1574
+ var import_promises = require("fs/promises");
1575
+ var import_node_path = require("path");
1576
+ var import_qrcode = __toESM(require("qrcode"));
1577
+ async function renderQr(data, outPath, opts = {}) {
1578
+ if (!outPath) {
1579
+ const text = await import_qrcode.default.toString(data, { type: "terminal", small: !!opts.small });
1580
+ return { kind: "terminal", text };
1581
+ }
1582
+ const ext = (0, import_node_path.extname)(outPath).toLowerCase();
1583
+ if (ext === ".svg") {
1584
+ const svg = await import_qrcode.default.toString(data, { type: "svg", margin: 2 });
1585
+ await (0, import_promises.writeFile)(outPath, svg, "utf8");
1586
+ } else {
1587
+ const buf = await import_qrcode.default.toBuffer(data, { type: "png", margin: 2, width: 512 });
1588
+ await (0, import_promises.writeFile)(outPath, buf);
1589
+ }
1590
+ return { kind: "file", path: outPath };
1591
+ }
1592
+
1593
+ // src/commands/build.ts
1594
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1595
+ var SUBMISSION_TERMINAL = ["finished", "errored", "canceled", "timed_out"];
1596
+ async function followSubmission(id) {
1597
+ let cursor;
1598
+ while (true) {
1599
+ try {
1600
+ const page = await (0, import_core3.getSubmissionLogs)(id, cursor);
1601
+ for (const l of page.lines || []) (0, import_core3.info)(l.text);
1602
+ if (page.nextCursor) cursor = page.nextCursor;
1603
+ } catch {
1604
+ }
1605
+ const { submission } = await (0, import_core3.getSubmission)(id);
1606
+ if (SUBMISSION_TERMINAL.includes(submission.status)) return submission;
1607
+ await sleep(2500);
1608
+ }
1609
+ }
1610
+ async function downloadArtifact(url, b) {
1611
+ const ext = b.buildType === "aab" ? "aab" : b.buildType === "ipa" ? "ipa" : b.buildType === "sim" ? "tar.gz" : "apk";
1612
+ const dest = path2.join(process.cwd(), `${b.id}.${ext}`);
1613
+ const res = await fetch(url);
1614
+ if (!res.ok) throw new import_core3.TileError(`Download failed: HTTP ${res.status}`, import_core3.ExitCode.ERROR);
1615
+ fs2.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
1616
+ return dest;
1617
+ }
1618
+ var SRC_EXCLUDES = ["node_modules", ".git", ".tile", "dist", ".expo", "build"];
1619
+ async function packAndUpload(dir, uploadUrl) {
1620
+ if (!fs2.existsSync(path2.join(dir, "package.json"))) {
1621
+ throw new import_core3.TileError(
1622
+ `No package.json in ${dir} \u2014 not an Expo project.`,
1623
+ import_core3.ExitCode.USAGE,
1624
+ "Run from your app dir, or pass --dir. `tile init` scaffolds one."
1625
+ );
1626
+ }
1627
+ const tgz = path2.join(os.tmpdir(), `tile-src-${Date.now()}.tgz`);
1628
+ const args = ["-czf", tgz, "-C", dir, ...SRC_EXCLUDES.flatMap((e) => ["--exclude", e]), "."];
1629
+ const r = (0, import_child_process.spawnSync)("tar", args, { stdio: "inherit" });
1630
+ if (r.status !== 0) throw new import_core3.TileError("Failed to pack the project (tar).", import_core3.ExitCode.ERROR);
1631
+ try {
1632
+ const body = fs2.readFileSync(tgz);
1633
+ let res;
1634
+ try {
1635
+ res = await fetch(uploadUrl, {
1636
+ method: "PUT",
1637
+ headers: { "Content-Type": "application/octet-stream" },
1638
+ body
1639
+ });
1640
+ } catch (e) {
1641
+ throw new import_core3.TileError(`Source upload failed: ${e.message}`, import_core3.ExitCode.NETWORK);
1642
+ }
1643
+ if (!res.ok) throw new import_core3.TileError(`Source upload failed: HTTP ${res.status}`, import_core3.ExitCode.ERROR);
1644
+ } finally {
1645
+ try {
1646
+ fs2.unlinkSync(tgz);
1647
+ } catch {
1648
+ }
1649
+ }
1650
+ }
1651
+ function shortLine(b) {
1652
+ const when = b.createdAt || b.queuedAt || "-";
1653
+ return `${b.id} ${b.platform ?? "-"} ${b.profile ?? "-"} ${b.status} ${when}`;
1654
+ }
1655
+ function simulatorInstallHint(b) {
1656
+ if (b.buildType !== "sim") return;
1657
+ (0, import_core3.hint)(
1658
+ "iOS simulator build (.app, unsigned). Download the artifact, then on a Mac:",
1659
+ " open -a Simulator # boot a simulator",
1660
+ ` tar -xzf <downloaded>.tar.gz && xcrun simctl install booted *.app`
1661
+ );
1662
+ }
1663
+ async function waitUntilLaunched(id, maxMs = 45e3) {
1664
+ const start = Date.now();
1665
+ let b = (await (0, import_core3.getBuild)(id)).build;
1666
+ while (b.status === "new" && Date.now() - start < maxMs) {
1667
+ await sleep(2500);
1668
+ b = (await (0, import_core3.getBuild)(id)).build;
1669
+ }
1670
+ return b;
1671
+ }
1672
+ async function watch(id, maxMs = 30 * 60 * 1e3) {
1673
+ const start = Date.now();
1674
+ let last = "";
1675
+ while (true) {
1676
+ const { build } = await (0, import_core3.getBuild)(id);
1677
+ if (build.status !== last) {
1678
+ (0, import_core3.info)(` ${build.status}`);
1679
+ last = build.status;
1680
+ }
1681
+ if ((0, import_core3.isTerminalStatus)(build.status)) return build;
1682
+ if (Date.now() - start > maxMs) {
1683
+ (0, import_core3.info)(` (still ${build.status} after ${Math.round(maxMs / 6e4)}m \u2014 stopping watch)`);
1684
+ return build;
1685
+ }
1686
+ await sleep(5e3);
1687
+ }
1688
+ }
1689
+ function registerBuildCommands(program2) {
1690
+ const build = program2.command("build").description("Native app builds (Android/iOS).");
1691
+ build.command("list").description("List builds for an app (newest first).").option("--app <appId>", "app id (or tile.json / TILE_APP)").action(async (opts) => {
1692
+ const app = (0, import_core3.resolveApp)(opts);
1693
+ const builds = await (0, import_core3.listBuilds)(app);
1694
+ if ((0, import_core3.isJsonMode)()) {
1695
+ (0, import_core3.out)(builds);
1696
+ return;
1697
+ }
1698
+ if (!builds.length) (0, import_core3.info)("No builds yet.");
1699
+ else for (const b of builds) (0, import_core3.info)(shortLine(b));
1700
+ (0, import_core3.hint)(`Next: \`tile build status <id>\` for details, or \`tile build create --platform android\`.`);
1701
+ });
1702
+ build.command("status <id>").description("Show a build\u2019s status, metadata, and (when finished) a download URL.").action(async (id) => {
1703
+ const { build: b, artifactUrl } = await (0, import_core3.getBuild)(id);
1704
+ if ((0, import_core3.isJsonMode)()) {
1705
+ (0, import_core3.out)({ build: b, artifactUrl });
1706
+ return;
1707
+ }
1708
+ (0, import_core3.info)(`${b.id} ${b.platform ?? "-"}/${b.profile ?? "-"} ${b.status}`);
1709
+ if (b.versionName) (0, import_core3.info)(` version: ${b.versionName}`);
1710
+ if (b.error) (0, import_core3.info)(` error: ${JSON.stringify(b.error)}`);
1711
+ if (artifactUrl) (0, import_core3.info)(` artifact: ${artifactUrl}`);
1712
+ if (b.status === "finished" && artifactUrl) {
1713
+ if (b.buildType === "sim") simulatorInstallHint(b);
1714
+ else (0, import_core3.hint)("Next: download the artifact, or `tile ota deploy` (M2).");
1715
+ } else if (!(0, import_core3.isTerminalStatus)(b.status)) (0, import_core3.hint)(`Next: \`tile build logs ${id}\` to tail, or \`tile build status ${id}\`.`);
1716
+ });
1717
+ build.command("logs <id>").description("Fetch build logs (use --after <seq> to page).").option("--after <seq>", "only logs after this sequence number", "0").action(async (id, opts) => {
1718
+ const res = await (0, import_core3.getLogs)(id, parseInt(opts.after, 10) || 0);
1719
+ (0, import_core3.out)(res);
1720
+ });
1721
+ build.command("cancel <id>").description("Cancel an in-progress build.").action(async (id) => {
1722
+ const { build: b } = await (0, import_core3.cancelBuild)(id);
1723
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(b);
1724
+ else (0, import_core3.info)(`\u2713 Cancel requested \u2014 ${b.id} is now ${b.status}.`);
1725
+ });
1726
+ build.command("create").description("Create (and launch) a native build.").requiredOption("--platform <platform>", "android | ios").option("--profile <profile>", "build profile (e.g. production, preview)").option("--build-type <type>", "apk | aab | ipa").option("--source <source>", "upload (local files, default) | object | git | save", "upload").option("--source-object <key>", "pre-staged GCS object key (for --source object)").option("--ref <ref>", "git ref (for --source git)").option("--save <saveId>", "AppSave id to build (for --source save; omit = latest save)").option("--dir <dir>", "project dir to pack for --source upload", ".").option("--simulator", "iOS: build an unsigned simulator .app (no signing or registered device needed)").addOption(new import_commander.Option("--watch", "poll until the build reaches a terminal status").hideHelp(isAgentMode())).action(
1727
+ async (opts) => {
1728
+ const app = (0, import_core3.resolveApp)(opts);
1729
+ if (opts.simulator && opts.platform !== "ios") {
1730
+ throw new import_core3.TileError("--simulator is only valid with --platform ios.", import_core3.ExitCode.USAGE);
1731
+ }
1732
+ if (opts.platform === "ios" && !opts.simulator) {
1733
+ (0, import_core3.hint)(
1734
+ "iOS signing needs an App Store Connect API key. Set it (incl. team id) with:",
1735
+ " tile build credentials set-ios-key --file <AuthKey.p8> --key-id <id> --issuer-id <id> --team-id <id>",
1736
+ " (check current creds: `tile build credentials show`)",
1737
+ "No signing? Build for the simulator instead: `tile build create --platform ios --simulator`."
1738
+ );
1739
+ } else if (opts.simulator) {
1740
+ (0, import_core3.info)("Simulator build \u2014 unsigned .app, no signing credentials or registered device needed.");
1741
+ }
1742
+ if (opts.source === "object" && !opts.sourceObject) {
1743
+ throw new import_core3.TileError("--source object requires --source-object <key>.", import_core3.ExitCode.USAGE);
1744
+ }
1745
+ if (opts.save && opts.source === "upload") opts.source = "save";
1746
+ const { build: b, sourceUploadUrl } = await (0, import_core3.createBuild)(app, {
1747
+ platform: opts.platform,
1748
+ profile: opts.profile,
1749
+ buildType: opts.buildType,
1750
+ source: opts.source,
1751
+ sourceObject: opts.sourceObject,
1752
+ ref: opts.ref,
1753
+ saveId: opts.save,
1754
+ simulator: opts.simulator
1755
+ });
1756
+ if (opts.watch && isAgentMode()) {
1757
+ (0, import_core3.info)(
1758
+ "Note: `--watch` is not available in the Tile AI sandbox (it would block your turn). The build will be launched \u2014 track it from the dashboard, or tell the developer to run `tile build status <id>`."
1759
+ );
1760
+ }
1761
+ const watching = !!opts.watch && !isAgentMode();
1762
+ const follow = async (id) => {
1763
+ if (!watching) {
1764
+ (0, import_core3.info)("Launching\u2026");
1765
+ const cur = await waitUntilLaunched(id);
1766
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(cur);
1767
+ else if (cur.status === "new")
1768
+ (0, import_core3.info)(`Created ${id} \u2014 still queuing on the server. Check \`tile build status ${id}\`.`);
1769
+ else (0, import_core3.info)(`\u2713 Launched ${id} (${b.platform}/${b.profile ?? "-"}) \u2014 status ${cur.status}.`);
1770
+ (0, import_core3.hint)(`Next: \`tile build status ${id}\` (or re-run with --watch), \`tile build logs ${id}\`.`);
1771
+ if ((0, import_core3.isTerminalStatus)(cur.status) && cur.status !== "finished") process.exitCode = import_core3.ExitCode.ERROR;
1772
+ return;
1773
+ }
1774
+ (0, import_core3.info)(`Launching ${id} \u2014 watching\u2026`);
1775
+ const final = await watch(id);
1776
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(final);
1777
+ else {
1778
+ (0, import_core3.info)(`Build ${final.id} ended: ${final.status}.`);
1779
+ if (final.status === "finished") simulatorInstallHint(final);
1780
+ }
1781
+ if (final.status !== "finished") process.exitCode = import_core3.ExitCode.ERROR;
1782
+ };
1783
+ if (sourceUploadUrl) {
1784
+ const dir = path2.resolve(process.cwd(), opts.dir);
1785
+ (0, import_core3.info)(`Packing ${dir} and uploading source\u2026`);
1786
+ await packAndUpload(dir, sourceUploadUrl);
1787
+ await (0, import_core3.startBuild)(b.id);
1788
+ await follow(b.id);
1789
+ return;
1790
+ }
1791
+ await follow(b.id);
1792
+ }
1793
+ );
1794
+ const creds = build.command("credentials").alias("creds").description("Signing credentials (Android keystore, iOS ASC key).");
1795
+ creds.command("show").description("Show the non-secret signing-credentials summary.").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1796
+ const app = (0, import_core3.resolveApp)(opts);
1797
+ const summary = await (0, import_core3.getCredentials)(app);
1798
+ const play = await (0, import_core3.getPlayCredentials)(app).catch(() => ({ serviceAccount: null }));
1799
+ (0, import_core3.out)({ ...summary, playServiceAccount: play.serviceAccount ?? null });
1800
+ (0, import_core3.hint)(
1801
+ "Set Android: `tile build credentials set-android-keystore --file <jks> \u2026`",
1802
+ "Set iOS: `tile build credentials set-ios-key --file <p8> --key-id \u2026 --issuer-id \u2026 --team-id \u2026`",
1803
+ "Change just the iOS team id (no re-upload): `tile build credentials set-ios-team-id <id>`",
1804
+ "Set Google Play (Android submit): `tile build credentials set-play --file <service-account.json>`"
1805
+ );
1806
+ });
1807
+ creds.command("set-android-keystore").description("Upload an Android keystore (becomes the default for builds).").requiredOption("--file <path>", "path to the .jks/.keystore file").requiredOption("--alias <alias>", "key alias").requiredOption("--store-password <pw>", "keystore password").option("--key-password <pw>", "key password (defaults to store password)").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1808
+ const keystoreBase64 = fs2.readFileSync(opts.file).toString("base64");
1809
+ const res = await (0, import_core3.uploadAndroidKeystore)((0, import_core3.resolveApp)(opts), {
1810
+ keystoreBase64,
1811
+ keyAlias: opts.alias,
1812
+ storePassword: opts.storePassword,
1813
+ keyPassword: opts.keyPassword
1814
+ });
1815
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1816
+ else (0, import_core3.info)("\u2713 Android keystore uploaded.");
1817
+ });
1818
+ creds.command("rm-android-keystore").description("Remove the Android keystore (next build auto-generates one).").option("--app <appId>", "app id (or selected app)").option("-y, --yes", "skip confirmation").action(async (opts) => {
1819
+ if (!opts.yes) throw new import_core3.TileError("Re-run with --yes to confirm.", import_core3.ExitCode.USAGE);
1820
+ const res = await (0, import_core3.deleteAndroidKeystore)((0, import_core3.resolveApp)(opts));
1821
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1822
+ else (0, import_core3.info)("\u2713 Android keystore removed.");
1823
+ });
1824
+ creds.command("set-ios-key").description("Upload + verify an App Store Connect API key (.p8).").requiredOption("--file <path>", "path to the AuthKey_*.p8 file").requiredOption("--key-id <id>", "ASC key id").requiredOption("--issuer-id <id>", "ASC issuer id").option("--team-id <id>", "Apple Developer Team ID (e.g. ABCDE12345) \u2014 pins iOS signing; patch later with `set-ios-team-id`").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1825
+ const p8Base64 = fs2.readFileSync(opts.file).toString("base64");
1826
+ const res = await (0, import_core3.uploadIosAscKey)((0, import_core3.resolveApp)(opts), {
1827
+ p8Base64,
1828
+ keyId: opts.keyId,
1829
+ issuerId: opts.issuerId,
1830
+ teamId: opts.teamId
1831
+ });
1832
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1833
+ else (0, import_core3.info)("\u2713 iOS ASC API key uploaded + verified.");
1834
+ });
1835
+ creds.command("set-ios-team-id <teamId>").description('Update ONLY the Apple Team ID on the existing ASC key (no .p8 re-upload). Pass "" to clear.').option("--app <appId>", "app id (or selected app)").action(async (teamId, opts) => {
1836
+ const res = await (0, import_core3.setIosTeamId)((0, import_core3.resolveApp)(opts), teamId);
1837
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1838
+ else (0, import_core3.info)(teamId ? `\u2713 iOS team id set to ${teamId}.` : "\u2713 iOS team id cleared.");
1839
+ });
1840
+ creds.command("rm-ios-key").description("Remove the iOS ASC API key.").option("--app <appId>", "app id (or selected app)").option("-y, --yes", "skip confirmation").action(async (opts) => {
1841
+ if (!opts.yes) throw new import_core3.TileError("Re-run with --yes to confirm.", import_core3.ExitCode.USAGE);
1842
+ const res = await (0, import_core3.deleteIosAscKey)((0, import_core3.resolveApp)(opts));
1843
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1844
+ else (0, import_core3.info)("\u2713 iOS ASC API key removed.");
1845
+ });
1846
+ creds.command("set-play <file>").description("Upload a Google Play service-account JSON (required for Android store submit).").option("--app <appId>", "app id (or selected app)").action(async (file, opts) => {
1847
+ const saJsonBase64 = fs2.readFileSync(file).toString("base64");
1848
+ const res = await (0, import_core3.uploadPlayServiceAccount)((0, import_core3.resolveApp)(opts), { saJsonBase64 });
1849
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1850
+ else (0, import_core3.info)(res.valid ? `\u2713 Play service account uploaded: ${res.message}` : `Uploaded: ${res.message ?? ""}`);
1851
+ });
1852
+ creds.command("rm-play").description("Remove the Google Play service account.").option("--app <appId>", "app id (or selected app)").option("-y, --yes", "skip confirmation").action(async (opts) => {
1853
+ if (!opts.yes) throw new import_core3.TileError("Re-run with --yes to confirm.", import_core3.ExitCode.USAGE);
1854
+ const res = await (0, import_core3.deletePlayServiceAccount)((0, import_core3.resolveApp)(opts));
1855
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1856
+ else (0, import_core3.info)("\u2713 Play service account removed.");
1857
+ });
1858
+ const env = build.command("env").description("Per-app build environment variables.");
1859
+ env.command("list").description("List environment variables (secret values omitted).").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1860
+ const vars = await (0, import_core3.listEnvVars)((0, import_core3.resolveApp)(opts));
1861
+ if ((0, import_core3.isJsonMode)()) {
1862
+ (0, import_core3.out)(vars);
1863
+ return;
1864
+ }
1865
+ if (!vars.length) (0, import_core3.info)("No env vars set.");
1866
+ else for (const v of vars) (0, import_core3.info)(`${v.id} ${v.key}=${v.value ?? "(secret)"} ${(v.environments || []).join(",") || "all"}`);
1867
+ (0, import_core3.hint)("Next: `tile build env set KEY=VALUE`.");
1868
+ });
1869
+ env.command("set <pair>").description("Create/update a variable (KEY=VALUE; upsert by key).").option("--visibility <v>", "plaintext | secret").option("--environments <list>", "comma-separated envs (e.g. preview,production)").option("--var-type <t>", "variable type").option("--app <appId>", "app id (or selected app)").action(async (pair, opts) => {
1870
+ const i = pair.indexOf("=");
1871
+ if (i < 0) throw new import_core3.TileError(`Bad pair "${pair}" \u2014 expected KEY=VALUE.`, import_core3.ExitCode.USAGE);
1872
+ const res = await (0, import_core3.upsertEnvVar)((0, import_core3.resolveApp)(opts), {
1873
+ key: pair.slice(0, i),
1874
+ value: pair.slice(i + 1),
1875
+ visibility: opts.visibility,
1876
+ environments: opts.environments ? opts.environments.split(",").map((s) => s.trim()) : void 0,
1877
+ varType: opts.varType
1878
+ });
1879
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1880
+ else (0, import_core3.info)(`\u2713 Set ${pair.slice(0, i)}.`);
1881
+ });
1882
+ env.command("rm <id>").description("Delete an environment variable by id.").option("--app <appId>", "app id (or selected app)").action(async (id, opts) => {
1883
+ await (0, import_core3.deleteEnvVar)((0, import_core3.resolveApp)(opts), id);
1884
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)({ ok: true, id });
1885
+ else (0, import_core3.info)(`\u2713 Removed env var ${id}.`);
1886
+ });
1887
+ const versioning = build.command("versioning").description("Build-number / versionCode management.");
1888
+ versioning.command("show").description("Show versioning settings for a platform.").option("--platform <platform>", "android | ios", "android").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1889
+ (0, import_core3.out)(await (0, import_core3.getVersioning)((0, import_core3.resolveApp)(opts), opts.platform));
1890
+ });
1891
+ versioning.command("set").description("Set versioning source / versionCode / auto-increment.").option("--platform <platform>", "android | ios", "android").option("--source <source>", "manual | remote").option("--version-code <n>", "explicit version code").option("--auto-increment", "auto-increment the version code per build").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1892
+ const res = await (0, import_core3.setVersioning)((0, import_core3.resolveApp)(opts), {
1893
+ platform: opts.platform,
1894
+ source: opts.source,
1895
+ versionCode: opts.versionCode != null ? parseInt(opts.versionCode, 10) : void 0,
1896
+ autoIncrement: opts.autoIncrement
1897
+ });
1898
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(res);
1899
+ else (0, import_core3.info)("\u2713 Versioning updated.");
1900
+ });
1901
+ build.command("eas-config").description("Show build profiles from the repo\u2019s eas.json.").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1902
+ (0, import_core3.out)(await (0, import_core3.getEasConfig)((0, import_core3.resolveApp)(opts)));
1903
+ });
1904
+ build.command("submit <buildId>").description("Submit a finished build to the store (iOS App Store/TestFlight, Android Play).").option("--mode <mode>", "upload | submit | both", "upload").option("--dest <dest>", "iOS destination: testflight | app-store").option("--track <track>", "Android track: internal | alpha | beta | production").option("--rollout <fraction>", "Android staged rollout fraction (0..1)").option("--no-wait", "return immediately (follow later with `tile build submissions logs <id>`)").action(
1905
+ async (buildId, opts) => {
1906
+ const mode = opts.mode === "both" ? "upload_and_submit" : opts.mode;
1907
+ const { submission } = await (0, import_core3.submitBuild)(buildId, {
1908
+ mode,
1909
+ destination: opts.dest ?? opts.track,
1910
+ rollout: opts.rollout != null ? parseFloat(opts.rollout) : void 0
1911
+ });
1912
+ (0, import_core3.info)(`Submission ${submission.id} \u2014 ${submission.platform} ${submission.mode} \u2192 ${submission.destination ?? "-"}`);
1913
+ if (!opts.wait) {
1914
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)({ submission, waited: false });
1915
+ else (0, import_core3.hint)(`Next: \`tile build submissions logs ${submission.id}\` to follow.`);
1916
+ return;
1917
+ }
1918
+ const s = await followSubmission(submission.id);
1919
+ const ok = s.status === "finished";
1920
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)({ ok, submission: s, error: ok ? null : s.error?.message ?? s.status });
1921
+ else (0, import_core3.info)(ok ? "\u2713 submitted" : `\u2717 ${s.status} ${s.error?.message ?? ""}`);
1922
+ if (!ok) process.exitCode = import_core3.ExitCode.ERROR;
1923
+ }
1924
+ );
1925
+ const submissions = build.command("submissions").description("Inspect store submissions.");
1926
+ submissions.command("list").description("List recent submissions (newest first).").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1927
+ const subs = await (0, import_core3.listSubmissions)((0, import_core3.resolveApp)(opts));
1928
+ if ((0, import_core3.isJsonMode)()) {
1929
+ (0, import_core3.out)(subs);
1930
+ return;
1931
+ }
1932
+ if (!subs.length) (0, import_core3.info)("No submissions yet.");
1933
+ else for (const s of subs) (0, import_core3.info)(`${s.id} ${s.status} ${s.platform} ${s.mode} \u2192 ${s.destination ?? "-"} ${s.createdAt}`);
1934
+ });
1935
+ submissions.command("view <id>").description("Show one submission.").action(async (id) => {
1936
+ const { submission: s } = await (0, import_core3.getSubmission)(id);
1937
+ if ((0, import_core3.isJsonMode)()) {
1938
+ (0, import_core3.out)(s);
1939
+ return;
1940
+ }
1941
+ (0, import_core3.info)(`${s.id} ${s.status} ${s.platform} ${s.mode} \u2192 ${s.destination ?? "-"}`);
1942
+ (0, import_core3.info)(` build: ${s.buildId ?? "-"} runner: ${s.runnerKind ?? "-"} ${s.runnerRef ?? ""}`);
1943
+ if (s.error) (0, import_core3.info)(` error: ${JSON.stringify(s.error)}`);
1944
+ });
1945
+ submissions.command("logs <id>").description("Stream a submission\u2019s logs until it finishes.").action(async (id) => {
1946
+ const s = await followSubmission(id);
1947
+ const ok = s.status === "finished";
1948
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)({ ok, submission: s });
1949
+ else (0, import_core3.info)(s.status);
1950
+ if (!ok) process.exitCode = import_core3.ExitCode.ERROR;
1951
+ });
1952
+ submissions.command("cancel <id>").description("Cancel a submission.").action(async (id) => {
1953
+ const { submission } = await (0, import_core3.cancelSubmission)(id);
1954
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(submission);
1955
+ else (0, import_core3.info)(`submission ${id} \u2192 ${submission.status}`);
1956
+ });
1957
+ const devices = build.command("devices").description("iOS devices for ad-hoc distribution.");
1958
+ devices.command("list").description("List iOS devices registered for ad-hoc builds.").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1959
+ const list = await (0, import_core3.listDevices)((0, import_core3.resolveApp)(opts));
1960
+ if ((0, import_core3.isJsonMode)()) {
1961
+ (0, import_core3.out)(list);
1962
+ return;
1963
+ }
1964
+ if (!list.length) (0, import_core3.info)("No devices registered \u2014 add one with `tile build devices create --udid <udid>`.");
1965
+ else for (const d of list) (0, import_core3.info)(`${d.id} ${d.udid} ${d.status} ${d.name ?? ""}`);
1966
+ });
1967
+ devices.command("create").description("Register an iOS device UDID with Apple for ad-hoc builds.").requiredOption("--udid <udid>", "device UDID (from Finder/Xcode/Settings)").option("--name <name>", "a friendly name for the device").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
1968
+ const { device } = await (0, import_core3.createDevice)((0, import_core3.resolveApp)(opts), { udid: opts.udid, name: opts.name });
1969
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)(device);
1970
+ else {
1971
+ (0, import_core3.info)(`\u2713 registered ${device.udid}${device.name ? ` (${device.name})` : ""} (id ${device.id}).`);
1972
+ (0, import_core3.hint)("It will be included in the next ad-hoc build\u2019s provisioning profile.");
1973
+ }
1974
+ });
1975
+ devices.command("remove <id>").description("Disable a device with Apple and remove it.").option("--app <appId>", "app id (or selected app)").action(async (id, opts) => {
1976
+ await (0, import_core3.removeDevice)((0, import_core3.resolveApp)(opts), id);
1977
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)({ ok: true, id });
1978
+ else (0, import_core3.info)(`\u2713 removed device ${id}.`);
1979
+ });
1980
+ build.command("qr <id>").description("Render a scannable QR for a finished build\u2019s install link.").option("-o, --output <file>", "write the QR to an image file (.png or .svg) instead of the terminal").option("--small", "compact half-block QR (smaller, but harder for cameras to scan)").option("--url", "print only the install URL (no QR)").action(async (id, opts) => {
1981
+ const { build: b, installUrl } = await (0, import_core3.getBuild)(id);
1982
+ if (!installUrl) throw new import_core3.TileError(`No install link (status: ${b.status}).`, import_core3.ExitCode.ERROR);
1983
+ if (opts.url) {
1984
+ (0, import_core3.out)(installUrl);
1985
+ return;
1986
+ }
1987
+ const rendered = await renderQr(installUrl, opts.output, { small: opts.small });
1988
+ if ((0, import_core3.isJsonMode)()) {
1989
+ (0, import_core3.out)({ installUrl, output: rendered.path ?? null });
1990
+ return;
1991
+ }
1992
+ if (rendered.kind === "file") (0, import_core3.info)(`QR written \u2192 ${rendered.path}`);
1993
+ else {
1994
+ (0, import_core3.info)(rendered.text);
1995
+ (0, import_core3.info)("hard to scan? shrink the terminal font so the whole QR fits, or use -o qr.png");
1996
+ }
1997
+ (0, import_core3.info)(`install: ${installUrl}`);
1998
+ (0, import_core3.hint)("Scan with a device camera to download & install the build.");
1999
+ });
2000
+ build.command("download <id>").description("Download a finished build artifact to the current directory.").action(async (id) => {
2001
+ const { build: b, artifactUrl } = await (0, import_core3.getBuild)(id);
2002
+ if (!artifactUrl) throw new import_core3.TileError(`No artifact (status: ${b.status}).`, import_core3.ExitCode.ERROR);
2003
+ const dest = await downloadArtifact(artifactUrl, b);
2004
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)({ ok: true, path: dest });
2005
+ else {
2006
+ (0, import_core3.info)(`\u2713 downloaded \u2192 ${dest}`);
2007
+ if (b.buildType === "sim") (0, import_core3.hint)(`Run: tar -xzf ${b.id}.tar.gz && xcrun simctl install booted *.app`);
2008
+ }
2009
+ });
2010
+ build.command("store-check <identifier>").description("Check whether a store app record exists (iOS bundle id / Android package).").option("--platform <p>", "ios | android", "ios").option("--app <appId>", "app id (or selected app)").action(async (identifier, opts) => {
2011
+ const r = await (0, import_core3.storeAppCheck)((0, import_core3.resolveApp)(opts), opts.platform, identifier);
2012
+ if (!r.reachable) throw new import_core3.TileError(`Could not reach the store API: ${r.message ?? ""}`, import_core3.ExitCode.NETWORK);
2013
+ if (!r.exists) throw new import_core3.TileError(`No store app record for ${identifier}${r.message ? ` \u2014 ${r.message}` : ""}`, import_core3.ExitCode.NOT_FOUND);
2014
+ if ((0, import_core3.isJsonMode)()) (0, import_core3.out)({ ...r, platform: opts.platform, identifier });
2015
+ else (0, import_core3.info)(`\u2713 app exists: ${r.name ?? identifier}${r.appId ? ` (${r.appId})` : ""}`);
2016
+ });
2017
+ }
2018
+
2019
+ // src/commands/meta.ts
2020
+ var import_core4 = __toESM(require_dist());
2021
+ var GUIDE = `tile \u2014 one CLI for a Tile (Expo/React Native) app.
2022
+
2023
+ Golden rules
2024
+ \u2022 Discover commands with --help; never guess flags.
2025
+ \u2022 Add --json to anything you parse (stdout = pure JSON, hints \u2192 stderr).
2026
+ \u2022 Read exit codes: 0 ok \xB7 2 usage \xB7 3 auth \xB7 4 not found \xB7 5 conflict \xB7 6 backend down.
2027
+ \u2022 Follow the "Next:" line each command prints.
2028
+
2029
+ Setup
2030
+ tile register create a new account (email + password), then log in
2031
+ tile login authenticate (or set TILE_TOKEN)
2032
+ tile whoami confirm the current user
2033
+ tile app list your apps; tile use <appId> selects the active one
2034
+ tile status where am I + the suggested next command
2035
+
2036
+ Lifecycle (available today)
2037
+ Apps tile app create --name \u2026 \xB7 tile app list \xB7 tile use <appId> \xB7 tile app delete <id> -y
2038
+ Local source tile blueprints (list starters) \xB7 tile init \xB7 tile pull \xB7 tile save \xB7 tile saves \xB7 tile revert
2039
+ \u25B8 \`tile init --name X\` \u2192 clones the \`default\` blueprint (Live Layer wired, appId injected)
2040
+ \`tile init --name X --blueprint <id>\` \u2192 clones a specific blueprint (see \`tile blueprints\`)
2041
+ \`tile init --name X --bare\` \u2192 a genuine BLANK Expo app (create-expo-app; no blueprint, no SDKs)
2042
+ \u25B8 Use \`tile init\` (NOT just \`tile app create\`) when you intend to edit files and \`tile save\`.
2043
+ \u25B8 History / undo: \`tile saves\` (newest first, * = current) \xB7 \`tile revert --to <saveId>\` re-saves an
2044
+ older version as the new current (history preserved, itself revertable); then \`tile pull\` to sync local.
2045
+ Native builds tile build
2046
+ list \xB7 status <id> \xB7 logs <id> \xB7 cancel <id> \xB7 create --platform android|ios [--watch]
2047
+ credentials (creds) show/set-android-keystore/set-ios-key \xB7 env list/set/rm \xB7 versioning show/set \xB7 eas-config
2048
+ Testing tile test \u2014 e2e (Maestro) on a REAL emulator
2049
+ (cloud) tile test run <buildId> --platform android \u2192 triggers a run; prints a test id
2050
+ tile test status <testId> \u2192 verdict (pass/fail) + what happened (lint/install/flows)
2051
+ tile test list [<buildId>] [--platform android] \xB7 tile test cancel <testId>
2052
+ (local) tile test [--lint] \u2014 run the flows in THIS environment (needs a device; not in the agent sandbox)
2053
+ OTA / code-push tile ota \u2026 (forwards to tile-push)
2054
+ ota deploy --platform android \xB7 ota bundle list \xB7 ota rollback <channel> \xB7 ota whoami
2055
+ GitHub tile github (alias gh) \u2014 two-way sync with a repo
2056
+ gh connect (browser OAuth) \xB7 gh status \xB7 gh repos \xB7 gh use <owner/repo> \xB7 gh disconnect
2057
+ gh push \u2192 mirror latest save to Git (Tile\u2192Git)
2058
+ gh pull \u2192 resync Git\u2192Tile (materialize branch HEAD into a save + write local; carries deps+assets;
2059
+ refuses a diverging pull without --force / last-writer-wins)
2060
+ gh drift \u2192 verify: has Git advanced past the latest cloud save? (in_sync / git_advanced / git_branch_gone)
2061
+ Integrations tile integrations (alias int) \u2014 connect backends/services for the AI agent
2062
+ int connect <supabase|shopify|custom> \xB7 int list \xB7 int disconnect <provider>
2063
+ \u2192 credentials entered in the terminal, stored in Secret Manager (NEVER in chat);
2064
+ the AI agent then uses connected integrations automatically (e.g. Supabase auth + DB).
2065
+
2066
+ Note: \`tile save\` stores to Tile's DB+storage (source of truth). \`tile ota\` ships JS bundles
2067
+ over-the-air to installed apps (different thing). GitHub sync is two-way: \`gh push\` (Tile\u2192Git),
2068
+ \`gh pull\` (Git\u2192Tile), \`gh drift\` (verify). OTA auth is currently tile-push's own token.
2069
+ Coming later: unified OTA auth (one login)
2070
+
2071
+ Run \`tile <group> --help\` for details. See AGENTS.md / SKILL.md for the full agent guide.`;
2072
+ function registerMetaCommands(program2) {
2073
+ program2.command("guide").description("What Tile is + the lifecycle + how to use this CLI.").action(() => {
2074
+ if ((0, import_core4.isJsonMode)()) {
2075
+ (0, import_core4.out)({
2076
+ summary: "tile \u2014 one CLI for a Tile (Expo/React Native) app.",
2077
+ groups: {
2078
+ auth: ["register", "login", "logout", "whoami"],
2079
+ app: ["app create", "app list", "app get", "app delete", "use"],
2080
+ source: ["blueprints", "init", "init --blueprint <id>", "init --bare", "pull", "save", "saves", "revert --to <saveId>"],
2081
+ build: ["list", "status", "logs", "cancel", "create", "credentials", "env", "versioning", "eas-config"],
2082
+ test: ["test run <id> --platform android", "test status <id>", "test list [<buildId>]", "test cancel <id>", "test --lint (local)"],
2083
+ ota: ["ota deploy", "ota bundle list", "ota rollback", "ota whoami"],
2084
+ github: ["github connect", "github status", "github repos", "github use", "github push", "github pull", "github drift", "github disconnect"],
2085
+ integrations: ["integrations connect <supabase|shopify|custom>", "integrations list", "integrations disconnect <provider>"],
2086
+ meta: ["guide", "status"]
2087
+ },
2088
+ exitCodes: { ok: 0, usage: 2, auth: 3, notFound: 4, conflict: 5, backendDown: 6 },
2089
+ jsonContract: "add --json for machine output; stdout=JSON, hints=stderr",
2090
+ notes: "tile save = store to Tile DB+storage (source of truth), NOT OTA code push",
2091
+ comingSoon: ["unified OTA auth (one login for tile ota)"]
2092
+ });
2093
+ } else {
2094
+ (0, import_core4.info)(GUIDE);
2095
+ }
2096
+ });
2097
+ program2.command("status").description("Show current state (app/env, auth, recent builds) + next step.").option("--app <appId>", "override app id").option("--env <env>", "override environment").action(async (opts) => {
2098
+ const appId = (0, import_core4.resolveAppOptional)(opts);
2099
+ const env = (0, import_core4.resolveEnvOptional)(opts);
2100
+ const hasToken = !!(0, import_core4.getToken)();
2101
+ const state = {
2102
+ appId: appId || null,
2103
+ env: env || null,
2104
+ authenticated: hasToken,
2105
+ next: []
2106
+ };
2107
+ if (hasToken) {
2108
+ try {
2109
+ state.user = await (0, import_core4.whoami)();
2110
+ } catch (e) {
2111
+ state.authenticated = false;
2112
+ }
2113
+ }
2114
+ if (appId && state.authenticated) {
2115
+ try {
2116
+ const builds = await (0, import_core4.listBuilds)(appId);
2117
+ state.recentBuilds = builds.slice(0, 3).map((b) => ({ id: b.id, platform: b.platform, status: b.status }));
2118
+ } catch (e) {
2119
+ state.recentBuilds = { error: e.message };
2120
+ }
2121
+ try {
2122
+ const tests = await (0, import_core4.listTests)(appId);
2123
+ state.recentTests = tests.slice(0, 3).map((t) => ({ id: t.id, status: t.status, result: t.result ?? null, buildId: t.buildId ?? null }));
2124
+ } catch (e) {
2125
+ state.recentTests = { error: e.message };
2126
+ }
2127
+ }
2128
+ if (!state.authenticated) state.next.push("tile login");
2129
+ if (!appId) state.next.push("set TILE_APP / --app, or add appId to tile.json");
2130
+ if (appId && !env) state.next.push("set TILE_ENV / --env (e.g. preview)");
2131
+ if (state.authenticated && appId && env) {
2132
+ state.next.push("tile build list --json");
2133
+ }
2134
+ if ((0, import_core4.isJsonMode)()) {
2135
+ (0, import_core4.out)(state);
2136
+ return;
2137
+ }
2138
+ (0, import_core4.info)(`App: ${state.appId ?? "(none \u2014 set TILE_APP, --app, or tile.json)"}`);
2139
+ (0, import_core4.info)(`Env: ${state.env ?? "(none \u2014 set TILE_ENV or --env)"}`);
2140
+ (0, import_core4.info)(`Auth: ${state.authenticated ? `${state.user?.email ?? "yes"}` : "not authenticated"}`);
2141
+ if (Array.isArray(state.recentBuilds)) {
2142
+ if (state.recentBuilds.length) {
2143
+ (0, import_core4.info)("Builds:");
2144
+ for (const b of state.recentBuilds) (0, import_core4.info)(` ${b.id} ${b.platform ?? "-"} ${b.status}`);
2145
+ } else {
2146
+ (0, import_core4.info)("Builds: none yet");
2147
+ }
2148
+ }
2149
+ if (Array.isArray(state.recentTests)) {
2150
+ if (state.recentTests.length) {
2151
+ (0, import_core4.info)("Tests:");
2152
+ for (const t of state.recentTests) {
2153
+ const verdict = t.result ? t.result === "passed" ? "\u2713 passed" : "\u2717 failed" : t.status;
2154
+ (0, import_core4.info)(` ${t.id} ${verdict} (build ${t.buildId ?? "-"})`);
2155
+ }
2156
+ } else {
2157
+ (0, import_core4.info)("Tests: none yet");
2158
+ }
2159
+ }
2160
+ (0, import_core4.hint)(`Next: ${state.next[0] ?? "tile status"}`);
2161
+ });
2162
+ }
2163
+
2164
+ // src/commands/app.ts
2165
+ var import_core5 = __toESM(require_dist());
2166
+ var path3 = __toESM(require("path"));
2167
+ function setActive(appId, name) {
2168
+ const cfgPath = (0, import_core5.findConfigPath)();
2169
+ if (cfgPath) {
2170
+ const cfg = (0, import_core5.readConfig)();
2171
+ cfg.appId = appId;
2172
+ if (name) cfg.name = name;
2173
+ (0, import_core5.linkProject)(appId, path3.dirname(cfgPath), name);
2174
+ return (0, import_core5.writeConfig)(cfg);
2175
+ }
2176
+ return (0, import_core5.writeGlobalConfig)({ appId, ...name ? { name } : {} });
2177
+ }
2178
+ function registerAppCommands(program2) {
2179
+ const app = program2.command("app").description("Create and manage Tile apps.");
2180
+ app.command("create").description("Create a new Tile app and select it.").requiredOption("--name <name>", "app name").option("--blueprint <id>", "blueprint to seed from (default: default)").action(async (opts) => {
2181
+ const res = await (0, import_core5.createApp)(opts.name, opts.blueprint);
2182
+ const where = setActive(res.app.id, res.app.name);
2183
+ if ((0, import_core5.isJsonMode)()) (0, import_core5.out)(res);
2184
+ else (0, import_core5.info)(`\u2713 Created app ${res.app.id} ("${res.app.name}") \u2014 selected (${where}).`);
2185
+ (0, import_core5.hint)(
2186
+ `Next: \`tile pull\` to fetch its files. (To edit + \`tile save\`, \`tile init\` scaffolds locally in one step.)`
2187
+ );
2188
+ });
2189
+ app.command("list").description("List your Tile apps.").action(async () => {
2190
+ const apps = await (0, import_core5.listApps)();
2191
+ if ((0, import_core5.isJsonMode)()) {
2192
+ (0, import_core5.out)(apps);
2193
+ return;
2194
+ }
2195
+ if (!apps.length) (0, import_core5.info)('No apps yet \u2014 `tile app create --name "My App"`.');
2196
+ else for (const a of apps) (0, import_core5.info)(`${a.id} ${a.name}`);
2197
+ (0, import_core5.hint)(`Next: \`tile use <appId>\` to select one.`);
2198
+ });
2199
+ app.command("get [id]").description("Show one app (defaults to the selected app).").action(async (id) => {
2200
+ const appId = id || (0, import_core5.readConfig)().appId;
2201
+ if (!appId) throw new import_core5.TileError("No app id given and none selected.", import_core5.ExitCode.USAGE, "Pass an id or run `tile use <appId>`.");
2202
+ const a = await (0, import_core5.getApp)(appId);
2203
+ (0, import_core5.out)(a);
2204
+ });
2205
+ app.command("delete <id>").description("Permanently delete an app and its files.").option("-y, --yes", "skip the confirmation prompt").action(async (id, opts) => {
2206
+ if (!opts.yes) {
2207
+ throw new import_core5.TileError(
2208
+ `Refusing to delete ${id} without confirmation.`,
2209
+ import_core5.ExitCode.USAGE,
2210
+ "Re-run with --yes to confirm."
2211
+ );
2212
+ }
2213
+ await (0, import_core5.deleteApp)(id);
2214
+ if ((0, import_core5.isJsonMode)()) (0, import_core5.out)({ ok: true, id });
2215
+ else (0, import_core5.info)(`\u2713 Deleted app ${id}.`);
2216
+ });
2217
+ program2.command("use <appId>").description("Select the active app (so build/push target it).").action(async (appId) => {
2218
+ let name;
2219
+ try {
2220
+ const apps = await (0, import_core5.listApps)();
2221
+ const match = apps.find((a) => a.id === appId);
2222
+ if (!match) {
2223
+ throw new import_core5.TileError(
2224
+ `App ${appId} is not in your account.`,
2225
+ import_core5.ExitCode.NOT_FOUND,
2226
+ "Run `tile app list` to see your apps."
2227
+ );
2228
+ }
2229
+ name = match.name;
2230
+ } catch (e) {
2231
+ if (e instanceof import_core5.TileError && e.exitCode === import_core5.ExitCode.NOT_FOUND) throw e;
2232
+ }
2233
+ const where = setActive(appId, name);
2234
+ if ((0, import_core5.isJsonMode)()) (0, import_core5.out)({ appId, name, config: where });
2235
+ else (0, import_core5.info)(`\u2713 Selected ${appId}${name ? ` ("${name}")` : ""} \u2014 saved to ${where}.`);
2236
+ (0, import_core5.hint)(`Next: \`tile status\` to confirm.`);
2237
+ });
2238
+ program2.command("projects").alias("ls").description("List apps linked to a local repo on this machine.").action(async () => {
2239
+ const reg = (0, import_core5.readProjectRegistry)();
2240
+ const entries = Object.entries(reg).sort(
2241
+ (a, b) => (b[1].lastUsedAt || "").localeCompare(a[1].lastUsedAt || "")
2242
+ );
2243
+ if ((0, import_core5.isJsonMode)()) {
2244
+ (0, import_core5.out)(entries.map(([id, l]) => ({ appId: id, ...l })));
2245
+ return;
2246
+ }
2247
+ if (!entries.length) {
2248
+ (0, import_core5.info)("No linked projects yet \u2014 `tile init` or `tile pull` links one.");
2249
+ return;
2250
+ }
2251
+ for (const [id, l] of entries) {
2252
+ const exists = (0, import_core5.resolveProjectPath)(id) ? "" : " (missing!)";
2253
+ (0, import_core5.info)(`${id} ${l.name ?? ""} ${l.path}${exists}`);
2254
+ }
2255
+ (0, import_core5.hint)(`Next: \`tile save --app <id>\` works from anywhere \xB7 \`cd "$(tile path <id>)"\`.`);
2256
+ });
2257
+ program2.command("path <appId>").description("Print the local repo path linked to an app.").action(async (appId) => {
2258
+ const p = (0, import_core5.resolveProjectPath)(appId);
2259
+ if (!p) {
2260
+ throw new import_core5.TileError(
2261
+ `No local repo linked for ${appId} on this machine.`,
2262
+ import_core5.ExitCode.NOT_FOUND,
2263
+ `Run \`tile pull --app ${appId}\` to fetch it, or \`tile projects\` to see links.`
2264
+ );
2265
+ }
2266
+ if ((0, import_core5.isJsonMode)()) (0, import_core5.out)({ appId, path: p });
2267
+ else process.stdout.write(p + "\n");
2268
+ });
2269
+ program2.command("unlink <appId>").description("Forget an app\u2192repo link on this machine (leaves files + app untouched).").action(async (appId) => {
2270
+ const removed = (0, import_core5.unlinkProject)(appId);
2271
+ if ((0, import_core5.isJsonMode)()) (0, import_core5.out)({ appId, unlinked: removed });
2272
+ else (0, import_core5.info)(removed ? `\u2713 Unlinked ${appId}.` : `No link for ${appId}.`);
2273
+ });
2274
+ }
2275
+
2276
+ // src/commands/source.ts
2277
+ var fs3 = __toESM(require("fs"));
2278
+ var path4 = __toESM(require("path"));
2279
+ var import_child_process2 = require("child_process");
2280
+ var import_core6 = __toESM(require_dist());
2281
+ var MANIFEST_DIR = ".tile";
2282
+ var MANIFEST_FILE = "manifest.json";
2283
+ var IGNORE = /* @__PURE__ */ new Set([
2284
+ "node_modules",
2285
+ ".git",
2286
+ ".tile",
2287
+ "dist",
2288
+ ".expo",
2289
+ "build",
2290
+ "tile.json",
2291
+ "TILE.md",
2292
+ // Local OTA/deploy tooling — never round-trip into the saved app source.
2293
+ ".env",
2294
+ "tile-push.config.ts"
2295
+ ]);
2296
+ function manifestPath(dir) {
2297
+ return path4.join(dir, MANIFEST_DIR, MANIFEST_FILE);
2298
+ }
2299
+ function readLocalManifest(dir) {
2300
+ try {
2301
+ return JSON.parse(fs3.readFileSync(manifestPath(dir), "utf8"));
2302
+ } catch {
2303
+ return {};
2304
+ }
2305
+ }
2306
+ function writeLocalManifest(dir, m) {
2307
+ const p = manifestPath(dir);
2308
+ fs3.mkdirSync(path4.dirname(p), { recursive: true });
2309
+ fs3.writeFileSync(p, JSON.stringify(m, null, 2) + "\n");
2310
+ }
2311
+ function writeTree(dir, files, appId) {
2312
+ const types = {};
2313
+ for (const [rel, f] of Object.entries(files)) {
2314
+ const dest = path4.join(dir, rel);
2315
+ fs3.mkdirSync(path4.dirname(dest), { recursive: true });
2316
+ if (f.type === "ASSET") fs3.writeFileSync(dest, Buffer.from(f.contents, "base64"));
2317
+ else fs3.writeFileSync(dest, f.contents);
2318
+ types[rel] = f.type;
2319
+ }
2320
+ writeLocalManifest(dir, { appId, types });
2321
+ return Object.keys(files).length;
2322
+ }
2323
+ async function writeSaveToDir(appId, saveId, dir) {
2324
+ const files = await (0, import_core6.getSaveFiles)(appId, saveId);
2325
+ fs3.mkdirSync(dir, { recursive: true });
2326
+ return writeTree(dir, files, appId);
2327
+ }
2328
+ function walk(dir, base = dir, acc = []) {
2329
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
2330
+ if (IGNORE.has(entry.name)) continue;
2331
+ const full = path4.join(dir, entry.name);
2332
+ if (entry.isDirectory()) walk(full, base, acc);
2333
+ else if (entry.isFile()) acc.push(path4.relative(base, full));
2334
+ }
2335
+ return acc;
2336
+ }
2337
+ var LIVE_LAYER_BASE = "https://storage.googleapis.com/tile-livelayer-configs";
2338
+ var WIRE_LIVE_LAYER = false;
2339
+ var SDK_NPM_VERSION = "^0.2.0";
2340
+ var OTA_API_URL = "https://ota.tile.dev";
2341
+ var TILE_PUSH_SDK_PKG = "@apptile/tile-push-react-native";
2342
+ var TILE_PUSH_SDK_VERSION = "^0.2.0";
2343
+ var TILE_PUSH_CLI_PKG = "@apptile/tile-push-cli";
2344
+ var TILE_PUSH_CLI_VERSION = "^0.1.0";
2345
+ var HOT_UPDATER_EXPO_PKG = "@hot-updater/expo";
2346
+ var HOT_UPDATER_EXPO_VERSION = "^0.32.0";
2347
+ var HOT_UPDATER_RN_PKG = "@hot-updater/react-native";
2348
+ var HOT_UPDATER_RN_VERSION = "^0.32.0";
2349
+ var HOT_UPDATER_CORE_PKG = "@hot-updater/core";
2350
+ var HOT_UPDATER_CORE_VERSION = "^0.32.0";
2351
+ var INJECT_OTA_RUNTIME = false;
2352
+ function wireProviders(dir, appId, env) {
2353
+ const candidate = ["App.js", "App.tsx", "App.ts", "app/_layout.tsx"].find(
2354
+ (f) => fs3.existsSync(path4.join(dir, f))
2355
+ );
2356
+ if (!candidate) return false;
2357
+ const p = path4.join(dir, candidate);
2358
+ let src = fs3.readFileSync(p, "utf8");
2359
+ if (src.includes("LiveLayerProvider")) return true;
2360
+ const marker = "export default function App";
2361
+ if (!src.includes(marker)) return false;
2362
+ src = src.replace(marker, "function BaseApp");
2363
+ let imports = "";
2364
+ if (WIRE_LIVE_LAYER) {
2365
+ imports += `import { LiveLayerProvider } from 'tile-live-layer';
2366
+ import tileBootstrap from './assets/tile-live-layer.json';
2367
+ `;
2368
+ }
2369
+ if (INJECT_OTA_RUNTIME) imports += `import { TilePush } from '${TILE_PUSH_SDK_PKG}';
2370
+ `;
2371
+ const liveLayerWrap = WIRE_LIVE_LAYER ? `
2372
+
2373
+ // --- Auto-wired by \`tile init\`: Tile Live Layer (remote config). appId injected. ---
2374
+ // Read remote config anywhere below with the useLiveLayer hook (path, fallback):
2375
+ // import { useLiveLayer } from 'tile-live-layer';
2376
+ // const mode = useLiveLayer(['home', 'mode'], 'upcoming');
2377
+ // Change it remotely (no rebuild) with: tile live-layer set home.mode=latest
2378
+ const tileLiveLayerOptions = {
2379
+ appId: '${appId}',
2380
+ env: '${env}',
2381
+ baseUrl: '${LIVE_LAYER_BASE}',
2382
+ bootstrap: tileBootstrap,
2383
+ // context: [], // optional category, e.g. ['in','mumbai']; omit for the base layer
2384
+ };
2385
+
2386
+ function TileApp(props) {
2387
+ return (
2388
+ <LiveLayerProvider options={tileLiveLayerOptions}>
2389
+ <BaseApp {...props} />
2390
+ </LiveLayerProvider>
2391
+ );
2392
+ }
2393
+ ` : `
2394
+
2395
+ function TileApp(props) {
2396
+ return (
2397
+ <BaseApp {...props} />
2398
+ );
2399
+ }
2400
+ `;
2401
+ const otaWrap = INJECT_OTA_RUNTIME ? `
2402
+ // --- Auto-wired by \`tile init\`: Tile Push (OTA / code-push). Same appId. ---
2403
+ export default TilePush.wrap({
2404
+ appId: '${appId}',
2405
+ apiUrl: '${OTA_API_URL}',
2406
+ updateStrategy: 'fingerprint',
2407
+ })(TileApp);
2408
+ ` : `
2409
+ export default TileApp;
2410
+ // To enable OTA (code-push) once \`${TILE_PUSH_SDK_PKG}\` is installed, wrap the
2411
+ // default export \u2014 same appId as live-layer:
2412
+ // import { TilePush } from '${TILE_PUSH_SDK_PKG}';
2413
+ // export default TilePush.wrap({ appId: '${appId}', apiUrl: '${OTA_API_URL}', updateStrategy: 'fingerprint' })(TileApp);
2414
+ // Manage releases with \`tile ota --help\` (deploy / rollback / rollout / enable).
2415
+ `;
2416
+ fs3.writeFileSync(p, imports + src + liveLayerWrap + otaWrap);
2417
+ return true;
2418
+ }
2419
+ function peerBelowFloor(range) {
2420
+ if (!range) return true;
2421
+ const m = /(\d+)\.(\d+)/.exec(range);
2422
+ if (!m) return false;
2423
+ const major = Number(m[1]);
2424
+ const minor = Number(m[2]);
2425
+ if (major !== 0) return false;
2426
+ return minor < 32;
2427
+ }
2428
+ function addSdkDependency(dir) {
2429
+ const pkgPath = path4.join(dir, "package.json");
2430
+ if (!fs3.existsSync(pkgPath)) return;
2431
+ try {
2432
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
2433
+ pkg.dependencies = pkg.dependencies || {};
2434
+ let changed = false;
2435
+ if (WIRE_LIVE_LAYER && !pkg.dependencies["tile-live-layer"]) {
2436
+ pkg.dependencies["tile-live-layer"] = SDK_NPM_VERSION;
2437
+ changed = true;
2438
+ }
2439
+ if (INJECT_OTA_RUNTIME) {
2440
+ if (!pkg.dependencies[TILE_PUSH_SDK_PKG]) {
2441
+ pkg.dependencies[TILE_PUSH_SDK_PKG] = TILE_PUSH_SDK_VERSION;
2442
+ changed = true;
2443
+ }
2444
+ if (peerBelowFloor(pkg.dependencies[HOT_UPDATER_RN_PKG])) {
2445
+ pkg.dependencies[HOT_UPDATER_RN_PKG] = HOT_UPDATER_RN_VERSION;
2446
+ changed = true;
2447
+ }
2448
+ if (!pkg.dependencies[HOT_UPDATER_CORE_PKG]) {
2449
+ pkg.dependencies[HOT_UPDATER_CORE_PKG] = HOT_UPDATER_CORE_VERSION;
2450
+ changed = true;
2451
+ }
2452
+ pkg.devDependencies = pkg.devDependencies || {};
2453
+ if (!pkg.devDependencies[TILE_PUSH_CLI_PKG] && !pkg.dependencies[TILE_PUSH_CLI_PKG]) {
2454
+ pkg.devDependencies[TILE_PUSH_CLI_PKG] = TILE_PUSH_CLI_VERSION;
2455
+ changed = true;
2456
+ }
2457
+ if (!pkg.devDependencies[HOT_UPDATER_EXPO_PKG] && !pkg.dependencies[HOT_UPDATER_EXPO_PKG]) {
2458
+ pkg.devDependencies[HOT_UPDATER_EXPO_PKG] = HOT_UPDATER_EXPO_VERSION;
2459
+ changed = true;
2460
+ }
2461
+ }
2462
+ if (changed) fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
2463
+ } catch {
2464
+ }
2465
+ }
2466
+ function detectBundler(dir) {
2467
+ try {
2468
+ const pkg = JSON.parse(fs3.readFileSync(path4.join(dir, "package.json"), "utf8"));
2469
+ const all = { ...pkg.dependencies, ...pkg.devDependencies };
2470
+ if (all.expo) return "expo";
2471
+ } catch {
2472
+ }
2473
+ return "expo";
2474
+ }
2475
+ function wireTilePushConfig(dir, appId) {
2476
+ const configPath = path4.join(dir, "tile-push.config.ts");
2477
+ if (!fs3.existsSync(configPath)) {
2478
+ const bundler = detectBundler(dir);
2479
+ const buildImport = bundler === "expo" ? `import { expo } from '@hot-updater/expo';` : `import { metro } from '@hot-updater/metro';`;
2480
+ const buildCall = bundler === "expo" ? `expo({ enableHermes: true })` : `metro()`;
2481
+ fs3.writeFileSync(
2482
+ configPath,
2483
+ `// Auto-wired by \`tile init\`. OTA deploys target this app id (the same id as
2484
+ // live-layer and the Tile backend). Resolved from TILE_PUSH_APP_ID in .env.
2485
+ import 'dotenv/config';
2486
+
2487
+ import { defineConfig } from 'hot-updater';
2488
+ ${buildImport}
2489
+ import { tilePushDatabase, tilePushStorage } from '${TILE_PUSH_CLI_PKG}';
2490
+
2491
+ const appId = process.env.TILE_PUSH_APP_ID;
2492
+ if (!appId) throw new Error('TILE_PUSH_APP_ID is not set (see .env).');
2493
+
2494
+ export default defineConfig({
2495
+ build: ${buildCall},
2496
+ storage: tilePushStorage({ appId }),
2497
+ database: tilePushDatabase({ appId }),
2498
+ updateStrategy: 'fingerprint',
2499
+ });
2500
+ `
2501
+ );
2502
+ }
2503
+ upsertEnvVar2(path4.join(dir, ".env"), "TILE_PUSH_APP_ID", appId);
2504
+ }
2505
+ function addAppJsonPlugin(dir) {
2506
+ const appJsonPath = path4.join(dir, "app.json");
2507
+ if (!fs3.existsSync(appJsonPath)) return;
2508
+ try {
2509
+ const appJson = JSON.parse(fs3.readFileSync(appJsonPath, "utf8"));
2510
+ const expo = appJson.expo || (appJson.expo = {});
2511
+ const plugins = Array.isArray(expo.plugins) ? expo.plugins : expo.plugins = [];
2512
+ const already = plugins.some(
2513
+ (p) => p === TILE_PUSH_SDK_PKG || Array.isArray(p) && p[0] === TILE_PUSH_SDK_PKG
2514
+ );
2515
+ if (already) return;
2516
+ plugins.push(TILE_PUSH_SDK_PKG);
2517
+ fs3.writeFileSync(appJsonPath, JSON.stringify(appJson, null, 2) + "\n");
2518
+ } catch {
2519
+ }
2520
+ }
2521
+ function regenerateLockfile(dir) {
2522
+ if (!fs3.existsSync(path4.join(dir, "package.json"))) return false;
2523
+ try {
2524
+ const res = (0, import_child_process2.spawnSync)(
2525
+ "npm",
2526
+ ["install", "--package-lock-only", "--no-audit", "--no-fund"],
2527
+ { cwd: dir, stdio: "ignore", env: process.env, timeout: 12e4 }
2528
+ );
2529
+ return res.status === 0;
2530
+ } catch {
2531
+ return false;
2532
+ }
2533
+ }
2534
+ function upsertEnvVar2(envPath, key, value) {
2535
+ let existing = "";
2536
+ try {
2537
+ existing = fs3.readFileSync(envPath, "utf8");
2538
+ } catch {
2539
+ }
2540
+ const lines = existing ? existing.split("\n") : [];
2541
+ const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
2542
+ if (idx >= 0) lines[idx] = `${key}=${value}`;
2543
+ else {
2544
+ if (existing && !existing.endsWith("\n")) lines.push("");
2545
+ lines.push(`${key}=${value}`);
2546
+ }
2547
+ fs3.writeFileSync(envPath, lines.join("\n").replace(/\n*$/, "\n"));
2548
+ }
2549
+ var RUNTIME_PROVIDED_DEPS = /* @__PURE__ */ new Set([
2550
+ "expo",
2551
+ "react",
2552
+ "react-native",
2553
+ "react-dom",
2554
+ "react-native-web",
2555
+ "tailwindcss",
2556
+ "nativewind",
2557
+ "tailwind-merge",
2558
+ "@expo/metro-runtime",
2559
+ "react-native-worklets",
2560
+ "react-native-worklets-core"
2561
+ ]);
2562
+ function depsFromPackageJson(dir, prev) {
2563
+ const pkgPath = path4.join(dir, "package.json");
2564
+ let pkgDeps = {};
2565
+ try {
2566
+ pkgDeps = JSON.parse(fs3.readFileSync(pkgPath, "utf8")).dependencies || {};
2567
+ } catch {
2568
+ return prev ?? {};
2569
+ }
2570
+ const out12 = {};
2571
+ for (const [name, versionRaw] of Object.entries(pkgDeps)) {
2572
+ if (RUNTIME_PROVIDED_DEPS.has(name)) continue;
2573
+ let version = String(versionRaw);
2574
+ if (version.startsWith("file:") || version.startsWith("link:") || version.startsWith("workspace:")) version = "*";
2575
+ const prevDep = prev?.[name];
2576
+ out12[name] = prevDep && prevDep.version === version ? prevDep : { version };
2577
+ }
2578
+ return out12;
2579
+ }
2580
+ var FOUNDATIONAL_EXPO = [
2581
+ "expo-modules-core",
2582
+ "expo-asset",
2583
+ "expo-font",
2584
+ "expo-constants",
2585
+ "expo-file-system",
2586
+ "expo-keep-awake",
2587
+ "expo-application",
2588
+ "expo-device",
2589
+ "expo-splash-screen",
2590
+ "expo-status-bar",
2591
+ "expo-linking",
2592
+ "expo-updates",
2593
+ "expo-manifests",
2594
+ "expo-eas-client",
2595
+ "expo-json-utils",
2596
+ "expo-structured-headers"
2597
+ ];
2598
+ var isExpoManaged = (n) => n === "expo" || n.startsWith("expo-") || n.startsWith("@expo/");
2599
+ function loadSdkChart(dir) {
2600
+ const candidates = [
2601
+ process.env.EXPO_SDK_CHART,
2602
+ "/agent/sdk54-native-modules.json",
2603
+ path4.join(dir, "node_modules/expo/bundledNativeModules.json")
2604
+ ].filter(Boolean);
2605
+ for (const p of candidates) {
2606
+ try {
2607
+ if (fs3.existsSync(p)) return JSON.parse(fs3.readFileSync(p, "utf8"));
2608
+ } catch {
2609
+ }
2610
+ }
2611
+ return null;
2612
+ }
2613
+ function normalizeExpoSdk(dir) {
2614
+ const pkgPath = path4.join(dir, "package.json");
2615
+ let text;
2616
+ try {
2617
+ text = fs3.readFileSync(pkgPath, "utf8");
2618
+ } catch {
2619
+ return;
2620
+ }
2621
+ const chart = loadSdkChart(dir);
2622
+ if (!chart) return;
2623
+ let pkg;
2624
+ try {
2625
+ pkg = JSON.parse(text);
2626
+ } catch {
2627
+ return;
2628
+ }
2629
+ const changes = [];
2630
+ for (const field of ["dependencies", "devDependencies"]) {
2631
+ const deps = pkg[field];
2632
+ if (!deps || typeof deps !== "object") continue;
2633
+ for (const name of Object.keys(deps)) {
2634
+ if (chart[name] && deps[name] !== chart[name]) {
2635
+ changes.push(`${name} ${deps[name]}\u2192${chart[name]}`);
2636
+ deps[name] = chart[name];
2637
+ }
2638
+ }
2639
+ }
2640
+ const declaredExpo = ["dependencies", "devDependencies"].flatMap((f) => Object.keys(pkg[f] || {})).filter(isExpoManaged);
2641
+ const overrides = pkg.overrides && typeof pkg.overrides === "object" ? pkg.overrides : {};
2642
+ for (const name of /* @__PURE__ */ new Set([...FOUNDATIONAL_EXPO, ...declaredExpo])) {
2643
+ if (chart[name] && overrides[name] !== chart[name]) {
2644
+ changes.push(`override ${name}=${chart[name]}`);
2645
+ overrides[name] = chart[name];
2646
+ }
2647
+ }
2648
+ if (Object.keys(overrides).length) {
2649
+ pkg.overrides = Object.fromEntries(Object.keys(overrides).sort().map((k) => [k, overrides[k]]));
2650
+ }
2651
+ if (!changes.length) return;
2652
+ const nl = text.endsWith("\n") ? "\n" : "";
2653
+ fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + nl);
2654
+ (0, import_core6.info)(`\u2713 Expo SDK lock: pinned ${changes.length} dep(s) to the supported SDK before save.`);
2655
+ }
2656
+ var BARE_APP_SOURCE = `import { StyleSheet, Text, View } from 'react-native';
2657
+
2658
+ export default function App() {
2659
+ return (
2660
+ <View style={styles.container}>
2661
+ <Text style={styles.title}>Hello from a blank Tile app \u{1F44B}</Text>
2662
+ <Text style={styles.body}>Edit App.js and \`tile save\` to publish.</Text>
2663
+ </View>
2664
+ );
2665
+ }
2666
+
2667
+ const styles = StyleSheet.create({
2668
+ container: { flex: 1, backgroundColor: '#0b1020', alignItems: 'center', justifyContent: 'center', padding: 24 },
2669
+ title: { fontSize: 22, fontWeight: '700', color: '#fff', marginBottom: 10 },
2670
+ body: { fontSize: 15, color: '#9fb3c8', textAlign: 'center' },
2671
+ });
2672
+ `;
2673
+ function makeWebReady(dir) {
2674
+ const indexJs = path4.join(dir, "index.js");
2675
+ if (fs3.existsSync(indexJs)) fs3.rmSync(indexJs);
2676
+ const pkgPath = path4.join(dir, "package.json");
2677
+ if (fs3.existsSync(pkgPath)) {
2678
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
2679
+ pkg.main = "node_modules/expo/AppEntry.js";
2680
+ pkg.dependencies = {
2681
+ ...pkg.dependencies || {},
2682
+ expo: "~54.0.25",
2683
+ react: "19.1.0",
2684
+ "react-dom": "19.1.0",
2685
+ "react-native": "0.81.5",
2686
+ "react-native-web": "~0.21.0"
2687
+ };
2688
+ delete pkg.dependencies["expo-status-bar"];
2689
+ fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
2690
+ }
2691
+ const appTsx = path4.join(dir, "App.tsx");
2692
+ const appFile = fs3.existsSync(appTsx) ? appTsx : path4.join(dir, "App.js");
2693
+ fs3.writeFileSync(appFile, BARE_APP_SOURCE);
2694
+ }
2695
+ function runBareExpo(dir) {
2696
+ const parent = path4.dirname(dir);
2697
+ const name = path4.basename(dir);
2698
+ fs3.mkdirSync(parent, { recursive: true });
2699
+ const r = (0, import_child_process2.spawnSync)(
2700
+ "npx",
2701
+ ["--yes", "create-expo-app@latest", name, "--template", "blank", "--no-install"],
2702
+ { cwd: parent, stdio: "inherit" }
2703
+ );
2704
+ if (r.status !== 0) return false;
2705
+ makeWebReady(dir);
2706
+ return true;
2707
+ }
2708
+ var ASSET_EXT = /\.(png|jpe?g|gif|webp|avif|ico|bmp|tiff?|ttf|otf|woff2?|eot|mp4|mov|webm|mp3|wav|ogg|m4a|aac|flac|pdf|zip|gz|bin|wasm|jks|keystore|p8|p12|mobileprovision)$/i;
2709
+ function readFileEntry(dir, rel) {
2710
+ const type = ASSET_EXT.test(rel) ? "ASSET" : "CODE";
2711
+ const abs = path4.join(dir, rel);
2712
+ const contents = type === "ASSET" ? fs3.readFileSync(abs).toString("base64") : fs3.readFileSync(abs, "utf8");
2713
+ return { contents, type };
2714
+ }
2715
+ function buildFileMap(dir) {
2716
+ const manifest = readLocalManifest(dir);
2717
+ const types = manifest.types || {};
2718
+ const map = {};
2719
+ for (const rel of walk(dir)) {
2720
+ const type = ASSET_EXT.test(rel) || types[rel] === "ASSET" ? "ASSET" : "CODE";
2721
+ const contents = type === "ASSET" ? fs3.readFileSync(path4.join(dir, rel)).toString("base64") : fs3.readFileSync(path4.join(dir, rel), "utf8");
2722
+ map[rel] = { contents, type };
2723
+ }
2724
+ return map;
2725
+ }
2726
+ function registerSourceCommands(program2) {
2727
+ program2.command("pull").description("Fetch the app\u2019s current files into a local directory.").option("--app <appId>", "app id (or selected app)").option("--out <dir>", "output directory", ".").action(async (opts) => {
2728
+ const appId = (0, import_core6.resolveApp)(opts);
2729
+ const files = await (0, import_core6.getCurrentFiles)(appId);
2730
+ const dir = path4.resolve(process.cwd(), opts.out);
2731
+ fs3.mkdirSync(dir, { recursive: true });
2732
+ const n = writeTree(dir, files, appId);
2733
+ (0, import_core6.linkProject)(appId, dir);
2734
+ if ((0, import_core6.isJsonMode)()) (0, import_core6.out)({ appId, files: n, dir });
2735
+ else (0, import_core6.info)(`\u2713 Pulled ${n} file(s) for ${appId} \u2192 ${opts.out}`);
2736
+ (0, import_core6.hint)(`Next: edit files, then \`tile save\` to store a new version.`);
2737
+ });
2738
+ program2.command("init").description("Create a new Tile app + scaffold it locally (from a blueprint, or --bare Expo).").requiredOption("--name <name>", "app name").option("--dir <dir>", "target directory (default: ./<name>)").option("--blueprint <id>", "blueprint to clone (see `tile blueprints`)", "default").option("--bare", "scaffold a genuine BLANK Expo app instead (create-expo-app; no Tile blueprint, no SDKs)").action(async (opts) => {
2739
+ const env = "preview";
2740
+ const dir = path4.resolve(process.cwd(), opts.dir || opts.name);
2741
+ if (opts.bare) {
2742
+ const res2 = await (0, import_core6.createApp)(opts.name);
2743
+ const appId2 = res2.app.id;
2744
+ (0, import_core6.info)("Scaffolding a blank Expo app (create-expo-app)\u2026");
2745
+ const ok = runBareExpo(dir);
2746
+ if (!ok) {
2747
+ throw new import_core6.TileError(
2748
+ "create-expo-app failed (needs network + npx).",
2749
+ import_core6.ExitCode.ERROR,
2750
+ "Retry, or use `tile init` (blueprint) instead of --bare."
2751
+ );
2752
+ }
2753
+ (0, import_core6.writeConfig)({ appId: appId2, name: res2.app.name, env }, dir);
2754
+ if (!(0, import_core6.findConfigPath)(dir)) (0, import_core6.writeGlobalConfig)({ appId: appId2, name: res2.app.name });
2755
+ (0, import_core6.linkProject)(appId2, dir, res2.app.name);
2756
+ fs3.writeFileSync(
2757
+ path4.join(dir, "TILE.md"),
2758
+ `# ${res2.app.name}
2759
+
2760
+ Tile app \`${appId2}\` (env \`${env}\`) \u2014 **blank Expo app** (no Tile SDKs).
2761
+
2762
+ This is a genuine create-expo-app blank project. Start from a batteries-included blueprint
2763
+ (\`tile blueprints\` \u2192 \`tile init --blueprint <id>\`) for a ready-made setup.
2764
+
2765
+ > Note: the app's saved source on Tile is the seeded default blueprint until you run \`tile save\` to push this blank tree.
2766
+
2767
+ ## Workflow
2768
+ - Save edits: \`tile save\`
2769
+ - Build: \`tile build create --platform android\`
2770
+ `
2771
+ );
2772
+ if ((0, import_core6.isJsonMode)()) (0, import_core6.out)({ app: res2.app, dir, bare: true });
2773
+ else {
2774
+ (0, import_core6.info)(`\u2713 Created ${appId2} ("${res2.app.name}") + blank Expo app \u2192 ${dir}`);
2775
+ (0, import_core6.info)(" \u21B3 Bare Expo (no Tile blueprint, no SDKs). `tile save` to push this tree as the app source.");
2776
+ }
2777
+ (0, import_core6.hint)(`Next: cd ${opts.dir || opts.name}, \`npm install\`, edit, then \`tile save\`.`);
2778
+ return;
2779
+ }
2780
+ const res = await (0, import_core6.createApp)(opts.name, opts.blueprint);
2781
+ const appId = res.app.id;
2782
+ fs3.mkdirSync(dir, { recursive: true });
2783
+ const files = await (0, import_core6.getFiles)(appId);
2784
+ const n = writeTree(dir, files, appId);
2785
+ (0, import_core6.writeConfig)({ appId, name: res.app.name, env }, dir);
2786
+ if (!(0, import_core6.findConfigPath)(dir)) (0, import_core6.writeGlobalConfig)({ appId, name: res.app.name });
2787
+ (0, import_core6.linkProject)(appId, dir, res.app.name);
2788
+ if (WIRE_LIVE_LAYER) {
2789
+ const assets = path4.join(dir, "assets");
2790
+ fs3.mkdirSync(assets, { recursive: true });
2791
+ fs3.writeFileSync(path4.join(assets, "tile-live-layer.json"), "{}\n");
2792
+ }
2793
+ const wired = wireProviders(dir, appId, env);
2794
+ addSdkDependency(dir);
2795
+ addAppJsonPlugin(dir);
2796
+ wireTilePushConfig(dir, appId);
2797
+ const lockOk = regenerateLockfile(dir);
2798
+ fs3.writeFileSync(
2799
+ path4.join(dir, "TILE.md"),
2800
+ `# ${res.app.name}
2801
+
2802
+ Tile app \`${appId}\` (env \`${env}\`) \u2014 blueprint \`${opts.blueprint}\`.
2803
+
2804
+ > One id everywhere: \`${appId}\` is this app's id for source/saves, builds, AND OTA (tile-push).
2805
+
2806
+ ` + (WIRE_LIVE_LAYER ? `## Tile Live Layer (remote config)
2807
+ ` + (wired ? `\`App\` is wrapped in \`<LiveLayerProvider>\` with this app's id already injected.
2808
+ Read flags with \`useLiveFlag('key', default)\` from \`tile-live-layer\`.
2809
+ ` : `Could not auto-wrap the root \u2014 wrap it manually in \`<LiveLayerProvider config={{ appId: "${appId}", env: "${env}", baseUrl: "${LIVE_LAYER_BASE}" }}>\`.
2810
+ `) + `
2811
+ > \`tile-live-layer\` (${SDK_NPM_VERSION}) is added to package.json \u2014 run \`npm install\`.
2812
+
2813
+ ` : ``) + `## Code push (OTA) \u2014 tile-push
2814
+ OTA targets the SAME app id (\`${appId}\`), authenticated with your \`tile login\` session; the
2815
+ tenant is auto-provisioned on first deploy. Releases are managed via:
2816
+
2817
+ - \`tile ota deploy --platform android --rollout 10\` \u2014 ship to 10% of devices
2818
+ - \`tile ota bundle list\` \u2014 see what's live \xB7 \`tile ota rollback production\` \u2014 pull latest
2819
+ - \`tile ota enable <id>\` / \`tile ota disable <id>\` \u2014 flip a bundle
2820
+ - \`tile ota --help\` \u2014 discover every command
2821
+
2822
+ ` + (INJECT_OTA_RUNTIME ? `The root is wrapped with \`TilePush.wrap(...)\` so devices auto-update on launch.
2823
+ ` : `> To make devices auto-update: install \`${TILE_PUSH_SDK_PKG}\` and wrap the default export (snippet in \`App\`).
2824
+ `) + `
2825
+ ## Workflow
2826
+ - Save edits: \`tile save\`
2827
+ - Build: \`tile build create --platform android\`
2828
+ - Ship JS-only update: \`tile ota deploy\`
2829
+ `
2830
+ );
2831
+ if ((0, import_core6.isJsonMode)())
2832
+ (0, import_core6.out)({ app: res.app, dir, files: n, blueprint: opts.blueprint, liveLayerWired: wired, otaAppId: appId });
2833
+ else {
2834
+ (0, import_core6.info)(`\u2713 Created ${appId} ("${res.app.name}") from blueprint \`${opts.blueprint}\` \u2014 ${n} file(s) \u2192 ${dir}`);
2835
+ (0, import_core6.info)(
2836
+ wired ? ` \u21B3 Live Layer + Tile Push OTA wired into App (appId injected).` : ` \u21B3 Could not auto-wrap App \u2014 see TILE.md for the manual snippet.`
2837
+ );
2838
+ (0, import_core6.info)(` \u21B3 OTA wired to the same id (${appId}) \u2014 SDK + config plugin + tile-push.config.ts written.`);
2839
+ (0, import_core6.info)(
2840
+ lockOk ? ` \u21B3 package-lock.json reconciled \u2014 \`npm ci\` and \`npm install\` both work.` : ` \u21B3 Could not refresh package-lock.json \u2014 run \`npm install\` before building.`
2841
+ );
2842
+ (0, import_core6.hint)(` \u21B3 Run \`npm install\`, then \`tile build create --platform android\`.`);
2843
+ }
2844
+ (0, import_core6.hint)(`Next: cd ${opts.dir || opts.name}, edit, then \`tile save\`. See TILE.md.`);
2845
+ });
2846
+ program2.command("blueprints").description("List the available starter blueprints (for `tile init --blueprint <id>`).").action(async () => {
2847
+ const bps = await (0, import_core6.listBlueprints)();
2848
+ if ((0, import_core6.isJsonMode)()) {
2849
+ (0, import_core6.out)(bps);
2850
+ return;
2851
+ }
2852
+ if (!bps.length) (0, import_core6.info)("No blueprints available.");
2853
+ else for (const b of bps) (0, import_core6.info)(`${b.id} ${b.emoji ?? ""} ${b.name}${b.description ? ` \u2014 ${b.description}` : ""}`);
2854
+ (0, import_core6.hint)("Next: `tile init --name <name> --blueprint <id>` (or `--bare` for a blank Expo app).");
2855
+ });
2856
+ program2.command("save").description("Save the local files as a new version of the app (Tile DB + storage).").option("--app <appId>", "app id (or selected app / tile.json)").option("--dir <dir>", "project directory", ".").option("--branch <branch>", "branch to attach the save to").action(async (opts) => {
2857
+ const dir = opts.dir !== "." || (0, import_core6.findConfigPath)() ? path4.resolve(process.cwd(), opts.dir) : (0, import_core6.resolveProjectDir)(opts);
2858
+ const localAppId = readLocalManifest(dir).appId || (0, import_core6.readConfig)(dir).appId;
2859
+ const appId = opts.app || localAppId || (0, import_core6.resolveApp)(opts);
2860
+ (0, import_core6.linkProject)(appId, dir);
2861
+ normalizeExpoSdk(dir);
2862
+ const files = buildFileMap(dir);
2863
+ if (!Object.keys(files).length) {
2864
+ throw new import_core6.TileError(`No files found under ${opts.dir}.`, import_core6.ExitCode.USAGE, "Run `tile pull` or `tile init` first.");
2865
+ }
2866
+ const meta = await (0, import_core6.getLatestSaveMeta)(appId);
2867
+ const res = await (0, import_core6.pushSave)(appId, {
2868
+ files,
2869
+ // Derive deps from the local package.json (not just the previous save's
2870
+ // deps) so newly-added packages like tile-live-layer actually persist.
2871
+ dependencies: depsFromPackageJson(dir, meta?.dependencies),
2872
+ sdkVersion: meta?.sdkVersion,
2873
+ branch: opts.branch ?? null
2874
+ });
2875
+ if ((0, import_core6.isJsonMode)()) (0, import_core6.out)(res);
2876
+ else (0, import_core6.info)(`\u2713 Saved version ${res.saveId} (${Object.keys(files).length} files).`);
2877
+ (0, import_core6.hint)(`Next: \`tile build create --platform android\` to build this version.`);
2878
+ });
2879
+ program2.command("saves").description("List saved versions (newest first). The current version is marked `*`.").option("--app <appId>", "app id (or selected app / tile.json)").option("--limit <n>", "max versions to show", "20").action(async (opts) => {
2880
+ const dir = process.cwd();
2881
+ const appId = opts.app || readLocalManifest(dir).appId || (0, import_core6.readConfig)(dir).appId || (0, import_core6.resolveApp)(opts);
2882
+ const ids = await (0, import_core6.listSaves)(appId);
2883
+ const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 20);
2884
+ const slice = ids.slice(0, limit);
2885
+ const manifests = await Promise.all(slice.map((id) => (0, import_core6.getSaveManifest)(appId, id).catch(() => null)));
2886
+ const rows = slice.map((id, i) => {
2887
+ const m = manifests[i] || {};
2888
+ return {
2889
+ saveId: id,
2890
+ current: i === 0,
2891
+ timestamp: m.timestamp ?? null,
2892
+ files: (m.files ?? []).length,
2893
+ changed: (m.deltaFiles ?? []).length
2894
+ };
2895
+ });
2896
+ if ((0, import_core6.isJsonMode)()) {
2897
+ (0, import_core6.out)({ appId, total: ids.length, saves: rows });
2898
+ return;
2899
+ }
2900
+ if (!rows.length) {
2901
+ (0, import_core6.info)("No saves yet.");
2902
+ (0, import_core6.hint)("Next: `tile save` to create the first version.");
2903
+ return;
2904
+ }
2905
+ for (const r of rows) {
2906
+ const when = r.timestamp ? r.timestamp.replace("T", " ").slice(0, 19) : "\u2014";
2907
+ (0, import_core6.info)(`${r.current ? "*" : " "} ${r.saveId} ${when} ${r.files} files (${r.changed} changed)`);
2908
+ }
2909
+ if (ids.length > rows.length) (0, import_core6.info)(` \u2026 ${ids.length - rows.length} older (use --limit ${ids.length}).`);
2910
+ (0, import_core6.hint)("Next: `tile revert --to <saveId>` to restore a version.");
2911
+ });
2912
+ program2.command("revert").description("Revert the app to a previous save by re-saving its files as a new version (history is preserved).").option("--app <appId>", "app id (or selected app / tile.json)").requiredOption("--to <saveId>", "the save id to restore (see `tile saves`)").option("--branch <branch>", "branch to attach the new save to (default: the target save's branch)").action(async (opts) => {
2913
+ const dir = process.cwd();
2914
+ const appId = opts.app || readLocalManifest(dir).appId || (0, import_core6.readConfig)(dir).appId || (0, import_core6.resolveApp)(opts);
2915
+ const ids = await (0, import_core6.listSaves)(appId);
2916
+ if (!ids.length) {
2917
+ throw new import_core6.TileError(`App ${appId} has no saves to revert to.`, import_core6.ExitCode.USAGE, "Run `tile save` first.");
2918
+ }
2919
+ if (!ids.includes(opts.to)) {
2920
+ throw new import_core6.TileError(`Save ${opts.to} not found for app ${appId}.`, import_core6.ExitCode.USAGE, "Run `tile saves` to list valid ids.");
2921
+ }
2922
+ if (ids[0] === opts.to) {
2923
+ throw new import_core6.TileError(`Save ${opts.to} is already the current version \u2014 nothing to revert.`, import_core6.ExitCode.USAGE);
2924
+ }
2925
+ const target2 = await (0, import_core6.getSaveManifest)(appId, opts.to);
2926
+ const files = await (0, import_core6.getSaveFiles)(appId, opts.to);
2927
+ if (!Object.keys(files).length) {
2928
+ throw new import_core6.TileError(`Save ${opts.to} has no files.`, import_core6.ExitCode.ERROR);
2929
+ }
2930
+ const branch = opts.branch ?? (typeof target2?.branch === "string" ? target2.branch : null);
2931
+ const res = await (0, import_core6.pushSave)(appId, {
2932
+ files,
2933
+ dependencies: target2?.dependencies,
2934
+ sdkVersion: target2?.sdkVersion,
2935
+ branch
2936
+ });
2937
+ const n = Object.keys(files).length;
2938
+ if ((0, import_core6.isJsonMode)()) (0, import_core6.out)({ appId, revertedFrom: ids[0], revertedTo: opts.to, newSaveId: res.saveId, files: n });
2939
+ else (0, import_core6.info)(`\u2713 Reverted to ${opts.to} \u2192 new version ${res.saveId} (${n} files).`);
2940
+ (0, import_core6.hint)("Next: `tile pull` to sync your local copy, or `tile build create --platform android`.");
2941
+ });
2942
+ }
2943
+
2944
+ // src/commands/dev.ts
2945
+ var http = __toESM(require("http"));
2946
+ var fs5 = __toESM(require("fs"));
2947
+ var path6 = __toESM(require("path"));
2948
+ var crypto2 = __toESM(require("crypto"));
2949
+
2950
+ // src/commands/devPhoneRelay.ts
2951
+ var os2 = __toESM(require("os"));
2952
+ var fs4 = __toESM(require("fs"));
2953
+ var path5 = __toESM(require("path"));
2954
+ var crypto = __toESM(require("crypto"));
2955
+ var import_socket = require("socket.io");
2956
+ var ASSET_MIME = {
2957
+ ".png": "image/png",
2958
+ ".jpg": "image/jpeg",
2959
+ ".jpeg": "image/jpeg",
2960
+ ".gif": "image/gif",
2961
+ ".webp": "image/webp",
2962
+ ".svg": "image/svg+xml",
2963
+ ".bmp": "image/bmp",
2964
+ ".ico": "image/x-icon",
2965
+ ".mp4": "video/mp4",
2966
+ ".webm": "video/webm",
2967
+ ".mov": "video/quicktime",
2968
+ ".m4v": "video/x-m4v",
2969
+ ".mp3": "audio/mpeg",
2970
+ ".wav": "audio/wav",
2971
+ ".ogg": "audio/ogg",
2972
+ ".m4a": "audio/mp4",
2973
+ ".aac": "audio/aac",
2974
+ ".flac": "audio/flac",
2975
+ ".ttf": "font/ttf",
2976
+ ".otf": "font/otf",
2977
+ ".woff": "font/woff",
2978
+ ".woff2": "font/woff2"
2979
+ };
2980
+ var assetMime = (ext) => ASSET_MIME[ext.toLowerCase()] || "application/octet-stream";
2981
+ function getLanIp() {
2982
+ const ifaces = os2.networkInterfaces();
2983
+ for (const name of Object.keys(ifaces)) {
2984
+ for (const ni of ifaces[name] ?? []) {
2985
+ if (ni.family === "IPv4" && !ni.internal) return ni.address;
2986
+ }
2987
+ }
2988
+ return null;
2989
+ }
2990
+ function readDependencies(dir) {
2991
+ try {
2992
+ const pkg = JSON.parse(fs4.readFileSync(path5.join(dir, "package.json"), "utf8"));
2993
+ return pkg.dependencies && typeof pkg.dependencies === "object" ? pkg.dependencies : void 0;
2994
+ } catch {
2995
+ return void 0;
2996
+ }
2997
+ }
2998
+ function buildCodeMessage(fileMap, dependencies, assetCtx) {
2999
+ const diff = {};
3000
+ const s3url = {};
3001
+ let skippedAssets = 0;
3002
+ for (const [p, entry] of Object.entries(fileMap)) {
3003
+ if (entry.type === "ASSET") {
3004
+ if (assetCtx) {
3005
+ const ext = path5.extname(p);
3006
+ const hash = crypto.createHash("sha256").update(entry.contents).digest("hex").slice(0, 16);
3007
+ const key = `${hash}${ext}`;
3008
+ assetCtx.index.set(key, { contents: entry.contents, mime: assetMime(ext) });
3009
+ diff[p] = "";
3010
+ s3url[p] = `${assetCtx.baseUrl}/${key}`;
3011
+ } else {
3012
+ skippedAssets++;
3013
+ }
3014
+ continue;
3015
+ }
3016
+ diff[p] = entry.contents;
3017
+ }
3018
+ if (skippedAssets > 0) {
3019
+ console.log(` \u26A0 phone preview: skipped ${skippedAssets} asset(s) (no LAN address to host them)`);
3020
+ }
3021
+ return { type: "CODE", diff, s3url, ...dependencies ? { dependencies } : {} };
3022
+ }
3023
+ var fmtBytes = (n) => n < 1024 ? `${n}b` : `${(n / 1024).toFixed(1)}kb`;
3024
+ var ts = () => {
3025
+ try {
3026
+ return (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
3027
+ } catch {
3028
+ return "";
3029
+ }
3030
+ };
3031
+ function attachPhoneRelay(server, dir, snapshot, opts = {}) {
3032
+ const debug = !!opts.debug;
3033
+ const dlog = (...m) => debug && console.log(` [stream ${ts()}]`, ...m);
3034
+ const io = new import_socket.Server(server, {
3035
+ // CORS-open like the SSE endpoints; native clients don't enforce it but
3036
+ // a browser-based run-engine (web player) would.
3037
+ cors: { origin: "*" },
3038
+ transports: ["websocket", "polling"],
3039
+ // The device answers heartbeats on its JS thread, which can be busy for a
3040
+ // few seconds during a heavy reload (large bundle transpile, full remount).
3041
+ // Be generous so a legitimate busy spell doesn't get mistaken for a dead
3042
+ // client and dropped — the run-engine also bounds its own fetches now.
3043
+ pingTimeout: 6e4,
3044
+ pingInterval: 25e3
3045
+ });
3046
+ const sockets = /* @__PURE__ */ new Set();
3047
+ const assetIndex = /* @__PURE__ */ new Map();
3048
+ const sendCodeTo = (socket) => {
3049
+ const channel = socket.data.channel;
3050
+ if (!channel) return;
3051
+ const deps = readDependencies(dir);
3052
+ const assetCtx = opts.assetBaseUrl ? { baseUrl: opts.assetBaseUrl, index: assetIndex } : void 0;
3053
+ const msg = buildCodeMessage(snapshot(), deps, assetCtx);
3054
+ socket.emit("message", { channel, message: msg, sender: "tiledev" });
3055
+ if (debug) {
3056
+ const files = Object.entries(msg.diff);
3057
+ const total = files.reduce((s, [, c]) => s + Buffer.byteLength(c), 0);
3058
+ const list = files.map(([p, c]) => `${p} ${fmtBytes(Buffer.byteLength(c))}`).join(", ");
3059
+ dlog(
3060
+ `\u2192 CODE sid=${socket.id.slice(0, 6)} files=${files.length} deps=${deps ? Object.keys(deps).length : 0} total=${fmtBytes(total)}
3061
+ [${list}]`
3062
+ );
3063
+ }
3064
+ };
3065
+ io.on("connection", (socket) => {
3066
+ sockets.add(socket);
3067
+ dlog(`device connected sid=${socket.id.slice(0, 6)} addr=${socket.handshake.address}`);
3068
+ socket.on("subscribeChannel", (data) => {
3069
+ socket.data.channel = data.channel;
3070
+ socket.join(data.channel);
3071
+ console.log(` \u{1F4F1} device subscribed (channel ${data.channel}, ${sockets.size} device(s))`);
3072
+ dlog(`\u2190 subscribe channel=${data.channel} sender=${data.sender ?? "?"}`);
3073
+ sendCodeTo(socket);
3074
+ });
3075
+ socket.on("unsubscribeChannel", (data) => {
3076
+ if (data?.channel) socket.leave(data.channel);
3077
+ dlog(`\u2190 unsubscribe channel=${data?.channel}`);
3078
+ });
3079
+ socket.on("message", (data) => {
3080
+ const m = data?.message ?? {};
3081
+ const type = m.type;
3082
+ if (type === "RESEND_CODE") {
3083
+ dlog("\u2190 RESEND_CODE (device asked for the latest code)");
3084
+ sendCodeTo(socket);
3085
+ return;
3086
+ }
3087
+ if (debug) {
3088
+ if (type === "CONSOLE") {
3089
+ const payload = Array.isArray(m.payload) ? m.payload.join(" ") : String(m.payload ?? "");
3090
+ const tag = m.method === "error" ? "\u{1F534}" : m.method === "warn" ? "\u{1F7E1}" : "\u26AA";
3091
+ dlog(`\u2190 ${tag} app.console.${m.method}:`, payload);
3092
+ } else if (type === "STATUS_REPORT") {
3093
+ dlog(`\u2190 STATUS_REPORT status=${JSON.stringify(m.status)}`);
3094
+ } else if (type === "DOM_DATA") {
3095
+ dlog(`\u2190 DOM_DATA (layers: ${m.data ? Object.keys(m.data).length : 0})`);
3096
+ } else if (type) {
3097
+ dlog(`\u2190 ${type}`);
3098
+ }
3099
+ }
3100
+ if (data?.channel) socket.to(data.channel).emit("message", data);
3101
+ });
3102
+ socket.on("disconnect", (reason) => {
3103
+ sockets.delete(socket);
3104
+ dlog(`device disconnected sid=${socket.id.slice(0, 6)} reason=${reason} (${sockets.size} left)`);
3105
+ });
3106
+ });
3107
+ return {
3108
+ broadcast() {
3109
+ if (debug && sockets.size === 0) dlog("file changed but no devices connected \u2014 nothing to push");
3110
+ for (const socket of sockets) sendCodeTo(socket);
3111
+ },
3112
+ deviceCount() {
3113
+ return sockets.size;
3114
+ },
3115
+ serveAsset(key) {
3116
+ return assetIndex.get(key);
3117
+ }
3118
+ };
3119
+ }
3120
+
3121
+ // src/commands/dev.ts
3122
+ var PORT_DEFAULT = 7163;
3123
+ var DEV_CHANNEL = "tiledev";
3124
+ var sha1 = (s) => crypto2.createHash("sha1").update(s).digest("hex");
3125
+ function isIgnored(rel) {
3126
+ if (/(^|[/\\])(node_modules|\.git|\.tile|dist|build|\.expo|\.next|coverage)([/\\]|$)/.test(rel)) return true;
3127
+ if (/\.tmp\.\d+\.[0-9a-f]+$/i.test(rel)) return true;
3128
+ return rel.split(/[/\\]/).some((seg) => seg.length > 1 && seg.startsWith("."));
3129
+ }
3130
+ function registerDevCommands(program2) {
3131
+ program2.command("dev", { hidden: isAgentMode() }).description("Run a local Dev Mode server that streams file saves to the Tile dashboard.").option("--dir <dir>", "project directory", ".").option("--port <port>", "local server port", String(PORT_DEFAULT)).option("--debug", "verbose stream logging: every CODE push (files+sizes), device connects, and the app's own console.log/warn/error + crashes surfaced in this terminal").action(async (opts) => {
3132
+ if (blockedInAgentMode(
3133
+ "dev",
3134
+ "Your edits already stream to the dashboard preview automatically \u2014 you don't need to watch or build. (Dev Mode / `tile dev` is for a human developer using their own IDE.)"
3135
+ )) {
3136
+ return;
3137
+ }
3138
+ const dir = path6.resolve(process.cwd(), opts.dir);
3139
+ const port = parseInt(opts.port, 10) || PORT_DEFAULT;
3140
+ const lanIp = getLanIp();
3141
+ const assetBaseUrl = lanIp ? `http://${lanIp}:${port}/__tiledev/~asset` : void 0;
3142
+ let phoneRelay;
3143
+ let appId;
3144
+ try {
3145
+ appId = JSON.parse(fs5.readFileSync(path6.join(dir, ".tile", "manifest.json"), "utf8")).appId;
3146
+ } catch {
3147
+ }
3148
+ const clients = /* @__PURE__ */ new Set();
3149
+ const lastWritten = /* @__PURE__ */ new Map();
3150
+ const readBody = (req) => new Promise((resolve7) => {
3151
+ let b = "";
3152
+ req.on("data", (c) => b += c);
3153
+ req.on("end", () => resolve7(b));
3154
+ });
3155
+ const sendAll = (obj) => {
3156
+ const line = `data: ${JSON.stringify(obj)}
3157
+
3158
+ `;
3159
+ for (const r of clients) {
3160
+ try {
3161
+ r.write(line);
3162
+ } catch {
3163
+ }
3164
+ }
3165
+ };
3166
+ const server = http.createServer((req, res) => {
3167
+ res.setHeader("Access-Control-Allow-Origin", "*");
3168
+ res.setHeader("Access-Control-Allow-Headers", "content-type");
3169
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
3170
+ if (req.method === "OPTIONS") {
3171
+ res.writeHead(204).end();
3172
+ return;
3173
+ }
3174
+ if ((req.url || "").startsWith("/__tiledev/~asset/")) {
3175
+ const key = (req.url || "").slice("/__tiledev/~asset/".length).split("?")[0];
3176
+ const a = phoneRelay?.serveAsset(key);
3177
+ if (!a) {
3178
+ res.writeHead(404).end();
3179
+ return;
3180
+ }
3181
+ const buf = Buffer.from(a.contents, "base64");
3182
+ res.writeHead(200, { "content-type": a.mime, "content-length": String(buf.length) });
3183
+ res.end(req.method === "HEAD" ? void 0 : buf);
3184
+ return;
3185
+ }
3186
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
3187
+ if (url.pathname === "/__tiledev/hello" || url.pathname === "/check-dev-mode") {
3188
+ res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
3189
+ res.end(JSON.stringify({
3190
+ ok: true,
3191
+ server: "tile-cli-dev",
3192
+ protocol: 1,
3193
+ appId: appId ?? null,
3194
+ dir,
3195
+ files: Object.keys(buildFileMap(dir)).length,
3196
+ // Scan-to-pair payload so the dashboard can render a "Connect preview"
3197
+ // QR in Dev Mode: the preview app reads relay host + channel from this
3198
+ // at runtime. Null when we couldn't determine a LAN IP (no phone path).
3199
+ channel: DEV_CHANNEL,
3200
+ pairUrl: lanIp ? `tiledev://${lanIp}:${port}?channel=${DEV_CHANNEL}` : null
3201
+ }));
3202
+ return;
3203
+ }
3204
+ if (url.pathname === "/__tiledev/events") {
3205
+ res.writeHead(200, {
3206
+ "content-type": "text/event-stream",
3207
+ "cache-control": "no-cache",
3208
+ connection: "keep-alive"
3209
+ });
3210
+ res.write("retry: 1000\n\n");
3211
+ clients.add(res);
3212
+ req.on("close", () => clients.delete(res));
3213
+ try {
3214
+ res.write(`data: ${JSON.stringify({ type: "snapshot", files: buildFileMap(dir) })}
3215
+
3216
+ `);
3217
+ } catch {
3218
+ }
3219
+ console.log(` \u2713 dashboard connected (${clients.size} client${clients.size === 1 ? "" : "s"})`);
3220
+ return;
3221
+ }
3222
+ if (url.pathname === "/__tiledev/write" && req.method === "POST") {
3223
+ readBody(req).then((body) => {
3224
+ let p;
3225
+ try {
3226
+ p = JSON.parse(body || "{}");
3227
+ } catch {
3228
+ res.writeHead(400).end("bad json");
3229
+ return;
3230
+ }
3231
+ const rel = String(p.path || "");
3232
+ const abs = path6.resolve(dir, rel);
3233
+ if (!rel || rel.startsWith("/") || abs !== dir && !abs.startsWith(dir + path6.sep)) {
3234
+ res.writeHead(400).end("bad path");
3235
+ return;
3236
+ }
3237
+ try {
3238
+ const contents = String(p.contents ?? "");
3239
+ fs5.mkdirSync(path6.dirname(abs), { recursive: true });
3240
+ if (p.type === "ASSET") fs5.writeFileSync(abs, Buffer.from(contents, "base64"));
3241
+ else fs5.writeFileSync(abs, contents, "utf8");
3242
+ lastWritten.set(rel, sha1(readFileEntry(dir, rel).contents));
3243
+ console.log(` \u2190 ${rel} (from design panel)`);
3244
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true }));
3245
+ } catch (e) {
3246
+ res.writeHead(500).end(`write failed: ${e?.message || e}`);
3247
+ }
3248
+ });
3249
+ return;
3250
+ }
3251
+ res.writeHead(404).end("not found");
3252
+ });
3253
+ phoneRelay = attachPhoneRelay(server, dir, () => buildFileMap(dir), { debug: !!opts.debug, assetBaseUrl });
3254
+ let broadcastTimer = null;
3255
+ const scheduleBroadcast = () => {
3256
+ if (broadcastTimer) clearTimeout(broadcastTimer);
3257
+ broadcastTimer = setTimeout(() => {
3258
+ broadcastTimer = null;
3259
+ phoneRelay.broadcast();
3260
+ }, 150);
3261
+ };
3262
+ const pending = /* @__PURE__ */ new Map();
3263
+ const onChange = (rel) => {
3264
+ if (!rel || isIgnored(rel)) return;
3265
+ const prev = pending.get(rel);
3266
+ if (prev) clearTimeout(prev);
3267
+ pending.set(rel, setTimeout(() => {
3268
+ pending.delete(rel);
3269
+ const abs = path6.join(dir, rel);
3270
+ if (!fs5.existsSync(abs)) {
3271
+ sendAll({ type: "unlink", path: rel });
3272
+ scheduleBroadcast();
3273
+ console.log(` \u2717 ${rel} (deleted)`);
3274
+ return;
3275
+ }
3276
+ try {
3277
+ const stat = fs5.statSync(abs);
3278
+ if (stat.isDirectory()) return;
3279
+ const { contents, type } = readFileEntry(dir, rel);
3280
+ if (lastWritten.get(rel) === sha1(contents)) {
3281
+ lastWritten.delete(rel);
3282
+ scheduleBroadcast();
3283
+ console.log(` \u2192 ${rel} (editor \u2192 preview)`);
3284
+ return;
3285
+ }
3286
+ sendAll({ type: "edit", path: rel, contents, fileType: type });
3287
+ scheduleBroadcast();
3288
+ console.log(` \u2192 ${rel}`);
3289
+ } catch {
3290
+ }
3291
+ }, 80));
3292
+ };
3293
+ try {
3294
+ fs5.watch(dir, { recursive: true }, (_event, filename) => {
3295
+ if (filename) onChange(String(filename));
3296
+ });
3297
+ } catch {
3298
+ console.error("\u26A0 recursive fs.watch is unavailable on this platform; nested-dir saves may not stream (macOS/Windows are fine).");
3299
+ }
3300
+ server.listen(port, async () => {
3301
+ const link = `http://localhost:${port}`;
3302
+ console.log(`\u25B8 tile dev \u2014 watching ${dir}${appId ? ` (app ${appId})` : ""}`);
3303
+ console.log(` Local dev server: ${link}`);
3304
+ console.log(' In the Tile dashboard editor, click "Dev Mode" and paste that URL.');
3305
+ console.log(" Then just save files in your IDE \u2014 the preview updates live. Ctrl-C to stop.");
3306
+ if (lanIp) {
3307
+ const relayUrl = `http://${lanIp}:${port}`;
3308
+ const pairUrl = `tiledev://${lanIp}:${port}?channel=${DEV_CHANNEL}`;
3309
+ console.log("");
3310
+ console.log(" \u2500\u2500 \u{1F4F1} Phone preview (same WiFi) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3311
+ console.log(` Relay: ${relayUrl} (channel ${DEV_CHANNEL})`);
3312
+ console.log(" Scan this QR in the preview app to connect:");
3313
+ console.log(` ${pairUrl}`);
3314
+ try {
3315
+ const qrt = require("qrcode-terminal");
3316
+ await new Promise(
3317
+ (resolve7) => qrt.generate(pairUrl, { small: true }, (s) => {
3318
+ console.log("\n" + s);
3319
+ resolve7();
3320
+ })
3321
+ );
3322
+ } catch {
3323
+ }
3324
+ console.log(` Or set env vars on a dev build: EXPO_PUBLIC_TILE_PACKET_PUB_URL=${relayUrl} EXPO_PUBLIC_TILE_DEV_CHANNEL=${DEV_CHANNEL}`);
3325
+ } else {
3326
+ console.log(" (no LAN IP found \u2014 phone preview unavailable on this network)");
3327
+ }
3328
+ });
3329
+ });
3330
+ }
3331
+
3332
+ // src/commands/preview.ts
3333
+ var http2 = __toESM(require("http"));
3334
+ var import_socket2 = require("socket.io");
3335
+ var PORT_DEFAULT2 = 4700;
3336
+ var CHANNEL = "tiledev";
3337
+ function registerPreviewCommands(program2) {
3338
+ program2.command("preview", { hidden: isAgentMode() }).description("Relay-only live preview: the dashboard streams code straight to a device (no local files).").option("--port <port>", "local relay port", String(PORT_DEFAULT2)).action(async (opts) => {
3339
+ if (blockedInAgentMode(
3340
+ "preview",
3341
+ "Your edits already stream to the dashboard preview automatically \u2014 `tile preview` is for a human pairing a phone to the dashboard."
3342
+ )) {
3343
+ return;
3344
+ }
3345
+ const port = parseInt(opts.port, 10) || PORT_DEFAULT2;
3346
+ const lanIp = getLanIp();
3347
+ let latestCode = { type: "CODE", diff: {}, s3url: {} };
3348
+ let pushCount = 0;
3349
+ const sockets = /* @__PURE__ */ new Set();
3350
+ const sendCodeTo = (socket) => {
3351
+ const channel = socket.data.channel;
3352
+ if (channel) socket.emit("message", { channel, message: latestCode, sender: "tilepreview" });
3353
+ };
3354
+ const server = http2.createServer((req, res) => {
3355
+ res.setHeader("Access-Control-Allow-Origin", "*");
3356
+ res.setHeader("Access-Control-Allow-Headers", "content-type");
3357
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
3358
+ if (req.method === "OPTIONS") {
3359
+ res.writeHead(204).end();
3360
+ return;
3361
+ }
3362
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
3363
+ if (url.pathname === "/__tiledev/hello") {
3364
+ res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
3365
+ res.end(JSON.stringify({
3366
+ ok: true,
3367
+ server: "tile-cli-preview",
3368
+ protocol: 1,
3369
+ channel: CHANNEL,
3370
+ pairUrl: lanIp ? `tiledev://${lanIp}:${port}?channel=${CHANNEL}` : null,
3371
+ devices: sockets.size
3372
+ }));
3373
+ return;
3374
+ }
3375
+ if (url.pathname === "/__tilepreview/push" && req.method === "POST") {
3376
+ let body = "";
3377
+ req.on("data", (c) => body += c);
3378
+ req.on("end", () => {
3379
+ try {
3380
+ const parsed = JSON.parse(body || "{}");
3381
+ latestCode = {
3382
+ type: "CODE",
3383
+ diff: parsed.diff && typeof parsed.diff === "object" ? parsed.diff : {},
3384
+ s3url: parsed.s3url && typeof parsed.s3url === "object" ? parsed.s3url : {},
3385
+ ...parsed.dependencies ? { dependencies: parsed.dependencies } : {}
3386
+ };
3387
+ pushCount++;
3388
+ for (const s of sockets) sendCodeTo(s);
3389
+ const n = Object.keys(latestCode.diff).length;
3390
+ console.log(` \u2192 push #${pushCount}: ${n} file(s) \u2192 ${sockets.size} device(s)`);
3391
+ res.writeHead(200, { "content-type": "application/json" });
3392
+ res.end(JSON.stringify({ ok: true, devices: sockets.size }));
3393
+ } catch (e) {
3394
+ res.writeHead(400, { "content-type": "application/json" });
3395
+ res.end(JSON.stringify({ ok: false, error: String(e?.message || e) }));
3396
+ }
3397
+ });
3398
+ return;
3399
+ }
3400
+ res.writeHead(404).end();
3401
+ });
3402
+ const io = new import_socket2.Server(server, {
3403
+ cors: { origin: "*" },
3404
+ transports: ["websocket", "polling"],
3405
+ pingTimeout: 6e4,
3406
+ pingInterval: 25e3
3407
+ });
3408
+ io.on("connection", (socket) => {
3409
+ sockets.add(socket);
3410
+ socket.on("subscribeChannel", (data) => {
3411
+ socket.data.channel = data.channel;
3412
+ socket.join(data.channel);
3413
+ console.log(` \u{1F4F1} device subscribed (channel ${data.channel}, ${sockets.size} device(s))`);
3414
+ sendCodeTo(socket);
3415
+ });
3416
+ socket.on("unsubscribeChannel", (data) => {
3417
+ if (data?.channel) socket.leave(data.channel);
3418
+ });
3419
+ socket.on("message", (data) => {
3420
+ if (data?.message?.type === "RESEND_CODE") sendCodeTo(socket);
3421
+ });
3422
+ socket.on("disconnect", () => {
3423
+ sockets.delete(socket);
3424
+ });
3425
+ });
3426
+ server.listen(port, () => {
3427
+ const relayUrl = lanIp ? `http://${lanIp}:${port}` : `http://localhost:${port}`;
3428
+ console.log("\u25B8 tile preview \u2014 relay-only live preview (no local files)");
3429
+ console.log(` Relay: ${relayUrl} (channel ${CHANNEL})`);
3430
+ console.log(` In the dashboard: open "Dev Preview" and paste http://localhost:${port}`);
3431
+ if (lanIp) {
3432
+ const pairUrl = `tiledev://${lanIp}:${port}?channel=${CHANNEL}`;
3433
+ console.log(" Or scan this QR in the preview app:");
3434
+ console.log(` ${pairUrl}`);
3435
+ try {
3436
+ const qrt = require("qrcode-terminal");
3437
+ qrt.generate(pairUrl, { small: true }, (s) => console.log("\n" + s));
3438
+ } catch {
3439
+ }
3440
+ }
3441
+ console.log(" Ctrl-C to stop.");
3442
+ });
3443
+ });
3444
+ }
3445
+
3446
+ // src/commands/ota.ts
3447
+ var fs6 = __toESM(require("fs"));
3448
+ var path7 = __toESM(require("path"));
3449
+ var import_child_process3 = require("child_process");
3450
+ var import_core7 = __toESM(require_dist());
3451
+ var TILE_PUSH_CLI_PKG2 = "@apptile/tile-push-cli";
3452
+ var TILE_PUSH_CLI_VERSION2 = "0.1.0";
3453
+ function resolveTilePush(dir) {
3454
+ const projectBin = path7.join(dir, "node_modules", ".bin", "tile-push");
3455
+ if (fs6.existsSync(projectBin)) return { cmd: projectBin, prefix: [] };
3456
+ const devBin = process.env.TILE_PUSH_CLI_BIN;
3457
+ if (devBin && fs6.existsSync(devBin)) return { cmd: process.execPath, prefix: [devBin] };
3458
+ return { cmd: "npx", prefix: ["-y", `${TILE_PUSH_CLI_PKG2}@${TILE_PUSH_CLI_VERSION2}`] };
3459
+ }
3460
+ var ALIASES = {
3461
+ enable: ["bundle", "enable"],
3462
+ disable: ["bundle", "disable"],
3463
+ promote: ["bundle", "promote"]
3464
+ };
3465
+ function rewriteAlias(args) {
3466
+ const [first, ...rest] = args;
3467
+ const mapped = first && ALIASES[first];
3468
+ return mapped ? [...mapped, ...rest] : args;
3469
+ }
3470
+ function runOta(args) {
3471
+ const dir = process.cwd();
3472
+ const { cmd, prefix } = resolveTilePush(dir);
3473
+ const env = { ...process.env };
3474
+ if (!env.TILE_PUSH_APP_ID) {
3475
+ const selected = (0, import_core7.resolveAppOptional)();
3476
+ if (selected) env.TILE_PUSH_APP_ID = selected;
3477
+ }
3478
+ if (env.TILE_PUSH_APP_ID && !env.TILE_PUSH_TOKEN) {
3479
+ const token = (0, import_core7.getToken)();
3480
+ if (token) env.TILE_PUSH_TOKEN = token;
3481
+ }
3482
+ const result = (0, import_child_process3.spawnSync)(cmd, [...prefix, ...rewriteAlias(args)], {
3483
+ cwd: dir,
3484
+ stdio: "inherit",
3485
+ env
3486
+ });
3487
+ if (result.error) {
3488
+ process.stderr.write(`error: could not run tile-push (${result.error.message})
3489
+ `);
3490
+ process.exit(6);
3491
+ }
3492
+ process.exit(result.status ?? 0);
3493
+ }
3494
+ function registerOtaCommands(program2) {
3495
+ const ota = program2.command("ota").description("Over-the-air (code-push) updates via tile-push \u2014 same appId as the rest of Tile.").addHelpText(
3496
+ "after",
3497
+ [
3498
+ "",
3499
+ "Identity: OTA targets the currently-selected Tile app (`tile use <appId>`),",
3500
+ "authenticated with your `tile login` session. The tenant is auto-provisioned",
3501
+ "on first deploy \u2014 no separate token or `tile-push init` step.",
3502
+ "",
3503
+ "Common flows:",
3504
+ " tile ota deploy --platform android --rollout 10 ship to 10% of devices",
3505
+ " tile ota deploy --platform android ship to everyone (100%)",
3506
+ " tile ota bundle list see what is live",
3507
+ " tile ota rollback production pull the latest bundle",
3508
+ " tile ota enable <bundle-id> / disable <bundle-id> flip a bundle on/off"
3509
+ ].join("\n")
3510
+ );
3511
+ ota.command("deploy").description("Build and ship a new JS bundle (supports staged --rollout).").option("-p, --platform <platform>", "ios | android").option("-t, --target-app-version <semver>", "target native app version (e.g. 1.0.0, 1.x.x)").option("-c, --channel <channel>", "release channel", "production").option("-r, --rollout <percentage>", "rollout percentage 0-100 (default 100)").option("-d, --disabled", "ship the bundle disabled").option("-f, --force-update", "require an immediate update on next launch").option("-m, --message <message>", "release notes (defaults to latest git commit)").action(() => {
3512
+ });
3513
+ ota.command("rollback").description("Disable the most recent enabled bundle on a channel (devices roll back).").argument("<channel>", "channel to roll back (e.g. production)").option("--platform <platform>", "ios | android").option("--target <bundle-id>", "scope to exactly this bundle id").option("-y, --yes", "skip confirmation").action(() => {
3514
+ });
3515
+ ota.command("enable").description("Re-enable a bundle by id (alias for `ota bundle enable`).").argument("<bundle-id>", "bundle id").option("-y, --yes", "skip confirmation").action(() => {
3516
+ });
3517
+ ota.command("disable").description("Disable a bundle by id (alias for `ota bundle disable`).").argument("<bundle-id>", "bundle id").option("-y, --yes", "skip confirmation").action(() => {
3518
+ });
3519
+ const bundle = ota.command("bundle").description("Inspect and manage individual bundles (list/show/enable/disable/update/promote/delete).");
3520
+ bundle.command("list").description("List bundles, most recent first.").option("-c, --channel <channel>", "filter by channel").option("--platform <platform>", "ios | android").option("--limit <n>", "max results (default 20)").option("--json", "raw JSON output").action(() => {
3521
+ });
3522
+ bundle.command("show").description("Show one bundle by id.").argument("<bundle-id>", "bundle id").option("--json", "raw JSON output").action(() => {
3523
+ });
3524
+ bundle.command("enable").description("Re-enable a bundle by id.").argument("<bundle-id>", "bundle id").option("-y, --yes", "skip confirmation").action(() => {
3525
+ });
3526
+ bundle.command("disable").description("Disable a bundle by id.").argument("<bundle-id>", "bundle id").option("-y, --yes", "skip confirmation").action(() => {
3527
+ });
3528
+ bundle.command("update").description("Adjust a live bundle: rollout cohorts, force-update, targeting.").argument("<bundle-id>", "bundle id").option("--rollout-cohort-count <count>", "rollout cohort count 0-1000 (staged rollout)").option("--force-update <true|false>", "toggle force-update").option("--target-cohorts <cohorts>", "comma-separated target cohorts").option("--clear-target-cohorts", "clear target cohorts").option("-y, --yes", "skip confirmation").option("--json", "raw JSON output").action(() => {
3529
+ });
3530
+ bundle.command("promote").description("Copy or move a bundle to another channel.").argument("<bundle-id>", "bundle id").requiredOption("-t, --target <channel>", "target channel").option("-a, --action <copy|move>", "copy (new id) or move (keep id)", "copy").option("-y, --yes", "skip confirmation").action(() => {
3531
+ });
3532
+ bundle.command("delete").description("Delete a bundle record by id.").argument("<bundle-id>", "bundle id").option("-y, --yes", "skip confirmation").action(() => {
3533
+ });
3534
+ const channel = ota.command("channel").description("Read/set the channel baked into the native app.");
3535
+ channel.command("set").description("Set the channel for Android (BuildConfig) and iOS (Info.plist).").argument("<channel>", "channel to set").action(() => {
3536
+ });
3537
+ const fingerprint = ota.command("fingerprint").description("Native fingerprint (update-eligibility) tooling.");
3538
+ fingerprint.command("create").description("Generate the native fingerprint for this project.").action(() => {
3539
+ });
3540
+ ota.command("whoami").description("Show the active OTA tenant + token (also provisions the tenant on first run).").action(() => {
3541
+ });
3542
+ ota.command("provision").description("Create the OTA tenant for the selected app now (runs whoami; idempotent).").action(() => {
3543
+ });
3544
+ ota.command("doctor").description("Diagnose OTA setup (config, credentials, bundler).").action(() => {
3545
+ });
3546
+ ota.command("info").description("Print resolved OTA settings and where each value came from.").action(() => {
3547
+ });
3548
+ ota.command("console").description("Open the OTA web console for the current tenant.").action(() => {
3549
+ });
3550
+ }
3551
+ var PROVISION_REWRITE = {
3552
+ provision: ["whoami"]
3553
+ };
3554
+ function runOtaIntercept(args) {
3555
+ const [first, ...rest] = args;
3556
+ const rewritten = first && PROVISION_REWRITE[first] ? [...PROVISION_REWRITE[first], ...rest] : args;
3557
+ if (first === "provision") {
3558
+ (0, import_core7.hint)("Provisioning the OTA tenant for the selected app (idempotent)\u2026");
3559
+ }
3560
+ runOta(rewritten);
3561
+ }
3562
+
3563
+ // src/commands/github.ts
3564
+ var import_child_process4 = require("child_process");
3565
+ var path8 = __toESM(require("path"));
3566
+ var import_core8 = __toESM(require_dist());
3567
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
3568
+ function openBrowser(url) {
3569
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3570
+ try {
3571
+ (0, import_child_process4.spawn)(cmd, [url], { stdio: "ignore", detached: true }).unref();
3572
+ } catch {
3573
+ }
3574
+ }
3575
+ function registerGithubCommands(program2) {
3576
+ const gh = program2.command("github").alias("gh").description("Connect a Tile app to a GitHub repo (mirror saves).");
3577
+ gh.command("status").description("Show the GitHub connection status for the app.").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
3578
+ const appId = (0, import_core8.resolveApp)(opts);
3579
+ const s = await (0, import_core8.getGithubStatus)(appId);
3580
+ if ((0, import_core8.isJsonMode)()) {
3581
+ (0, import_core8.out)(s);
3582
+ return;
3583
+ }
3584
+ if (!s.connected) {
3585
+ (0, import_core8.info)("GitHub: not connected.");
3586
+ (0, import_core8.hint)("Next: `tile github connect` to authorize.");
3587
+ return;
3588
+ }
3589
+ (0, import_core8.info)(`GitHub: connected${s.githubLogin ? ` as ${s.githubLogin}` : ""}.`);
3590
+ (0, import_core8.info)(` repo: ${s.repo || "(none \u2014 run `tile github use <owner/repo>`)"}`);
3591
+ (0, import_core8.info)(` branch: ${s.defaultBranch}`);
3592
+ (0, import_core8.hint)(s.repo ? "Next: `tile github push` to mirror the latest save." : "Next: `tile github repos` then `tile github use <owner/repo>`.");
3593
+ });
3594
+ gh.command("connect").description("Authorize GitHub via the browser, then wait until connected.").option("--app <appId>", "app id (or selected app)").option("--no-open", "don't auto-open the browser; just print the URL").option("--timeout <seconds>", "how long to wait for authorization", "180").action(async (opts) => {
3595
+ const appId = (0, import_core8.resolveApp)(opts);
3596
+ const existing = await (0, import_core8.getGithubStatus)(appId);
3597
+ if (existing.connected) {
3598
+ (0, import_core8.info)(`Already connected${existing.githubLogin ? ` as ${existing.githubLogin}` : ""}.`);
3599
+ (0, import_core8.hint)("Run `tile github disconnect` first if you want to reconnect.");
3600
+ return;
3601
+ }
3602
+ const url = await (0, import_core8.getInstallUrl)(appId);
3603
+ (0, import_core8.info)("Authorize Tile on GitHub (you must be logged into the Tile web dashboard in this browser):");
3604
+ (0, import_core8.info)(` ${url}`);
3605
+ if (opts.open !== false) openBrowser(url);
3606
+ const timeoutMs = (parseInt(opts.timeout, 10) || 180) * 1e3;
3607
+ const start = Date.now();
3608
+ (0, import_core8.info)("Waiting for authorization\u2026");
3609
+ while (Date.now() - start < timeoutMs) {
3610
+ await sleep2(3e3);
3611
+ let s;
3612
+ try {
3613
+ s = await (0, import_core8.getGithubStatus)(appId);
3614
+ } catch {
3615
+ continue;
3616
+ }
3617
+ if (s.connected) {
3618
+ if ((0, import_core8.isJsonMode)()) (0, import_core8.out)(s);
3619
+ else (0, import_core8.info)(`\u2713 Connected${s.githubLogin ? ` as ${s.githubLogin}` : ""}.`);
3620
+ (0, import_core8.hint)("Next: `tile github repos` then `tile github use <owner/repo>`.");
3621
+ return;
3622
+ }
3623
+ }
3624
+ throw new import_core8.TileError(
3625
+ "Timed out waiting for GitHub authorization.",
3626
+ import_core8.ExitCode.ERROR,
3627
+ "Re-run `tile github connect`; ensure you completed the browser flow while logged into the Tile web dashboard."
3628
+ );
3629
+ });
3630
+ gh.command("disconnect").description("Remove the GitHub connection for the app.").option("--app <appId>", "app id (or selected app)").option("-y, --yes", "skip the confirmation prompt").action(async (opts) => {
3631
+ const appId = (0, import_core8.resolveApp)(opts);
3632
+ if (!opts.yes) {
3633
+ throw new import_core8.TileError(
3634
+ `Refusing to disconnect GitHub for ${appId} without confirmation.`,
3635
+ import_core8.ExitCode.USAGE,
3636
+ "Re-run with --yes to confirm."
3637
+ );
3638
+ }
3639
+ await (0, import_core8.disconnectGithub)(appId);
3640
+ if ((0, import_core8.isJsonMode)()) (0, import_core8.out)({ ok: true, appId });
3641
+ else (0, import_core8.info)(`\u2713 Disconnected GitHub for ${appId}.`);
3642
+ });
3643
+ gh.command("repos").description("List repos the connected GitHub account can access.").option("--app <appId>", "app id (or selected app)").option("--page <n>", "page number", "1").action(async (opts) => {
3644
+ const appId = (0, import_core8.resolveApp)(opts);
3645
+ const { repos, hasMore } = await (0, import_core8.listUserRepos)(appId, parseInt(opts.page, 10) || 1);
3646
+ if ((0, import_core8.isJsonMode)()) {
3647
+ (0, import_core8.out)(repos);
3648
+ return;
3649
+ }
3650
+ if (!repos.length) (0, import_core8.info)("No repos visible to the connected token.");
3651
+ else for (const r of repos) (0, import_core8.info)(`${r.fullName}${r.private ? " (private)" : ""}`);
3652
+ (0, import_core8.hint)(`${hasMore ? "(more \u2014 use --page) " : ""}Next: \`tile github use <owner/repo>\`.`);
3653
+ });
3654
+ gh.command("use <ownerRepo>").description("Select the repo to sync with (owner/repo).").option("--app <appId>", "app id (or selected app)").action(async (ownerRepo, opts) => {
3655
+ const appId = (0, import_core8.resolveApp)(opts);
3656
+ const slash = ownerRepo.indexOf("/");
3657
+ if (slash < 0) throw new import_core8.TileError("Expected <owner/repo>.", import_core8.ExitCode.USAGE);
3658
+ const owner = ownerRepo.slice(0, slash);
3659
+ const repo = ownerRepo.slice(slash + 1);
3660
+ const res = await (0, import_core8.connectRepo)(appId, owner, repo);
3661
+ if ((0, import_core8.isJsonMode)()) (0, import_core8.out)(res);
3662
+ else (0, import_core8.info)(`\u2713 Connected ${res.repo} (default branch ${res.defaultBranch}).`);
3663
+ (0, import_core8.hint)("Next: `tile github push` to mirror the latest save to the repo.");
3664
+ });
3665
+ gh.command("push").description("Push the latest save to the connected GitHub repo.").option("--app <appId>", "app id (or selected app)").option("-b, --branch <branch>", "target branch (default: the repo default)").option("-m, --message <message>", "commit message").action(async (opts) => {
3666
+ const appId = (0, import_core8.resolveApp)(opts);
3667
+ const res = await (0, import_core8.githubPush)(appId, { branch: opts.branch, message: opts.message });
3668
+ if ((0, import_core8.isJsonMode)()) (0, import_core8.out)(res);
3669
+ else (0, import_core8.info)(`\u2713 Pushed${res.branch ? ` to ${res.branch}` : ""}${res.saveId ? ` (save ${res.saveId})` : ""}.`);
3670
+ });
3671
+ gh.command("pull").description("Resync from GitHub: materialize the branch HEAD into a new save and write it locally (GitHub \u2192 Tile).").option("--app <appId>", "app id (or selected app)").option("-b, --branch <branch>", "branch to pull (default: the repo default)").option("--out <dir>", "output directory", ".").option("-f, --force", "proceed even if the cloud has diverged from git (last-writer-wins)").action(async (opts) => {
3672
+ const appId = (0, import_core8.resolveApp)(opts);
3673
+ const status = await (0, import_core8.getGithubStatus)(appId);
3674
+ if (!status.connected || !status.repo) {
3675
+ throw new import_core8.TileError(
3676
+ `App ${appId} is not connected to a GitHub repo.`,
3677
+ import_core8.ExitCode.USAGE,
3678
+ "Run `tile github connect` then `tile github use <owner/repo>`."
3679
+ );
3680
+ }
3681
+ const branch = opts.branch || status.defaultBranch;
3682
+ const drift = await (0, import_core8.getBranchDrift)(appId, branch);
3683
+ if (drift.reason === "git_branch_gone") {
3684
+ throw new import_core8.TileError(
3685
+ `Branch ${branch} no longer exists on GitHub.`,
3686
+ import_core8.ExitCode.NOT_FOUND,
3687
+ "Pick another branch with --branch, or recreate it on GitHub."
3688
+ );
3689
+ }
3690
+ if (drift.hasDrift && drift.unpushedSaveCount > 0 && !opts.force) {
3691
+ throw new import_core8.TileError(
3692
+ `Cloud has diverged from git on ${branch}: ${drift.unpushedSaveCount} cloud save(s) are not reflected in git. Pulling makes git's state current; those saves stay in history (revertable via \`tile saves\`/\`tile revert\`).`,
3693
+ import_core8.ExitCode.CONFLICT,
3694
+ "Re-run with --force to proceed (last-writer-wins)."
3695
+ );
3696
+ }
3697
+ const mat = await (0, import_core8.materializeBranch)(appId, branch);
3698
+ const dir = path8.resolve(process.cwd(), opts.out);
3699
+ const n = await writeSaveToDir(appId, mat.saveId, dir);
3700
+ if ((0, import_core8.isJsonMode)()) {
3701
+ (0, import_core8.out)({ appId, branch, saveId: mat.saveId, baseGitSha: mat.baseGitSha, cached: mat.cached, files: n, dir });
3702
+ return;
3703
+ }
3704
+ (0, import_core8.info)(
3705
+ `\u2713 Resynced ${branch}@${mat.baseGitSha.slice(0, 7)} \u2192 save ${mat.saveId} (${n} files${mat.cached ? ", cached" : ""}) \u2192 ${opts.out}`
3706
+ );
3707
+ (0, import_core8.hint)("Next: edit, then `tile save`. Or `tile build create --platform android`.");
3708
+ });
3709
+ gh.command("drift").description("Check whether the GitHub branch has advanced past the latest cloud save (verify).").option("--app <appId>", "app id (or selected app)").option("-b, --branch <branch>", "branch to check (default: the repo default)").action(async (opts) => {
3710
+ const appId = (0, import_core8.resolveApp)(opts);
3711
+ const status = await (0, import_core8.getGithubStatus)(appId);
3712
+ if (!status.connected || !status.repo) {
3713
+ throw new import_core8.TileError(
3714
+ `App ${appId} is not connected to a GitHub repo.`,
3715
+ import_core8.ExitCode.USAGE,
3716
+ "Run `tile github connect` then `tile github use <owner/repo>`."
3717
+ );
3718
+ }
3719
+ const branch = opts.branch || status.defaultBranch;
3720
+ const d = await (0, import_core8.getBranchDrift)(appId, branch);
3721
+ if ((0, import_core8.isJsonMode)()) {
3722
+ (0, import_core8.out)({ appId, branch, ...d });
3723
+ return;
3724
+ }
3725
+ if (!d.hasDrift) {
3726
+ (0, import_core8.info)(`\u2713 In sync on ${branch}${d.reason === "no_cloud_saves" ? " (no cloud saves yet)" : ""}.`);
3727
+ } else if (d.reason === "git_branch_gone") {
3728
+ (0, import_core8.info)(`\u26A0 Branch ${branch} was deleted on GitHub.`);
3729
+ } else {
3730
+ (0, import_core8.info)(
3731
+ `\u26A0 Drift on ${branch}: git advanced to ${d.currentGitSha?.slice(0, 7)}; ${d.unpushedSaveCount} cloud save(s) on old base ${d.cloudBaseSha?.slice(0, 7)}.`
3732
+ );
3733
+ (0, import_core8.hint)("Next: `tile github pull` to resync (last-writer-wins).");
3734
+ }
3735
+ });
3736
+ }
3737
+
3738
+ // src/commands/integrations.ts
3739
+ var readline2 = __toESM(require("readline"));
3740
+ var import_stream2 = require("stream");
3741
+ var import_core9 = __toESM(require_dist());
3742
+ function prompt2(question, mask = false) {
3743
+ if (!process.stdin.isTTY) {
3744
+ throw new import_core9.TileError(
3745
+ "Cannot prompt for input: this is not an interactive terminal.",
3746
+ import_core9.ExitCode.USAGE,
3747
+ "Pass the values non-interactively with repeated --field key=value flags."
3748
+ );
3749
+ }
3750
+ return new Promise((resolve7) => {
3751
+ let muted = false;
3752
+ const mutableOut = new import_stream2.Writable({
3753
+ write(chunk, _enc, cb) {
3754
+ if (!muted) process.stderr.write(chunk);
3755
+ cb();
3756
+ }
3757
+ });
3758
+ const rl = readline2.createInterface({ input: process.stdin, output: mutableOut, terminal: true });
3759
+ process.stderr.write(question);
3760
+ if (mask) muted = true;
3761
+ rl.question("", (answer) => {
3762
+ if (mask) process.stderr.write("\n");
3763
+ rl.close();
3764
+ resolve7(answer.trim());
3765
+ });
3766
+ });
3767
+ }
3768
+ function parseFieldFlags(raw) {
3769
+ const out12 = {};
3770
+ for (const kv of raw || []) {
3771
+ const i = kv.indexOf("=");
3772
+ if (i <= 0) throw new import_core9.TileError(`Bad --field "${kv}" (expected key=value).`, import_core9.ExitCode.USAGE);
3773
+ out12[kv.slice(0, i)] = kv.slice(i + 1);
3774
+ }
3775
+ return out12;
3776
+ }
3777
+ var collectField = (val, prev) => [...prev, val];
3778
+ function registerIntegrationsCommands(program2) {
3779
+ const integ = program2.command("integrations").alias("int").description("Connect third-party integrations (Supabase, Shopify, custom) for an app.");
3780
+ integ.command("list").description("Show connected integrations and what is available.").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
3781
+ const appId = (0, import_core9.resolveApp)(opts);
3782
+ const data = await (0, import_core9.listIntegrations)(appId);
3783
+ if ((0, import_core9.isJsonMode)()) {
3784
+ (0, import_core9.out)(data);
3785
+ return;
3786
+ }
3787
+ (0, import_core9.info)(`Secret store: ${data.secretStore}`);
3788
+ (0, import_core9.info)("");
3789
+ (0, import_core9.info)("Connected:");
3790
+ if (!data.connected.length) (0, import_core9.info)(" (none)");
3791
+ for (const c of data.connected) {
3792
+ const pub = Object.entries(c.publicConfig).map(([k, v]) => `${k}=${v}`).join(", ");
3793
+ (0, import_core9.info)(` ${c.provider}${c.category ? `/${c.category}` : ""} [${c.status}] ${pub}`);
3794
+ if (c.secretKeys.length) (0, import_core9.info)(` secrets: ${c.secretKeys.join(", ")} (stored)`);
3795
+ }
3796
+ (0, import_core9.info)("");
3797
+ (0, import_core9.info)("Available providers:");
3798
+ for (const s of data.available) (0, import_core9.info)(` ${s.provider} (${s.category}) \u2014 fields: ${s.fields.map((f) => f.key + (f.secret ? "*" : "")).join(", ")}`);
3799
+ (0, import_core9.info)(" custom \u2014 any category, give --field key=value (prefix public: for non-secret)");
3800
+ (0, import_core9.hint)("Connect: `tile integrations connect <provider> --app <id>` (you'll be prompted for keys).");
3801
+ });
3802
+ integ.command("connect <provider>").description("Connect a provider (supabase | shopify | custom). Prompts for credentials.").option("--app <appId>", "app id (or selected app)").option("--category <category>", "category (required for custom)").option("--field <key=value>", "set a field non-interactively (repeatable)", collectField, []).action(async (provider, opts) => {
3803
+ const appId = (0, import_core9.resolveApp)(opts);
3804
+ const fields = parseFieldFlags(opts.field);
3805
+ let category = opts.category;
3806
+ if (provider === "custom") {
3807
+ if (!category) category = await prompt2("Category (e.g. payments, analytics): ");
3808
+ if (!category) throw new import_core9.TileError("Custom integration requires a category.", import_core9.ExitCode.USAGE);
3809
+ if (!Object.keys(fields).length) {
3810
+ (0, import_core9.info)('Enter credential fields. Blank key to finish. Prefix a key with "public:" if it is NOT secret.');
3811
+ while (true) {
3812
+ const key = await prompt2(" field key (blank to finish): ");
3813
+ if (!key) break;
3814
+ const secret = !key.startsWith("public:");
3815
+ const value = await prompt2(` value for ${key}${secret ? " (hidden)" : ""}: `, secret);
3816
+ if (value) fields[key] = value;
3817
+ }
3818
+ }
3819
+ } else {
3820
+ const { available } = await (0, import_core9.listIntegrations)(appId);
3821
+ const spec = available.find((s) => s.provider === provider);
3822
+ if (!spec) {
3823
+ throw new import_core9.TileError(
3824
+ `Unknown provider "${provider}". Known: ${available.map((s) => s.provider).join(", ")}, or "custom".`,
3825
+ import_core9.ExitCode.USAGE
3826
+ );
3827
+ }
3828
+ for (const f of spec.fields) {
3829
+ if (fields[f.key] !== void 0) continue;
3830
+ if (!process.stdin.isTTY) continue;
3831
+ const meta = [f.optional ? "optional" : null, f.example ? `e.g. ${f.example}` : null].filter(Boolean).join(", ");
3832
+ if (f.help) (0, import_core9.info)(` ${f.label}: ${f.help}`);
3833
+ const v = await prompt2(`${f.label}${meta ? ` (${meta})` : ""}${f.secret ? " (hidden)" : ""}: `, f.secret);
3834
+ if (v) fields[f.key] = v;
3835
+ }
3836
+ }
3837
+ const res = await (0, import_core9.connectIntegration)(appId, provider, { category, fields });
3838
+ if ((0, import_core9.isJsonMode)()) {
3839
+ (0, import_core9.out)(res);
3840
+ return;
3841
+ }
3842
+ (0, import_core9.info)(`\u2713 ${res.updated ? "Updated" : "Connected"} ${provider}${res.category ? `/${res.category}` : ""}.`);
3843
+ const stored = res.secretKeys?.length ? ` Secrets stored: ${res.secretKeys.join(", ")}.` : "";
3844
+ (0, import_core9.info)(` Public config: ${Object.entries(res.publicConfig || {}).map(([k, v]) => `${k}=${v}`).join(", ") || "(none)"}.${stored}`);
3845
+ (0, import_core9.hint)("The AI agent will now use this connection automatically when relevant.");
3846
+ });
3847
+ integ.command("resolve <provider>").description("Fetch a connection's config + secrets (for the AI agent / scripts). Prints secrets \u2014 use with care.").option("--app <appId>", "app id (or selected app)").option("--category <category>", "category (for custom providers)").action(async (provider, opts) => {
3848
+ const appId = (0, import_core9.resolveApp)(opts);
3849
+ const r = await (0, import_core9.resolveIntegration)(appId, provider, opts.category);
3850
+ if ((0, import_core9.isJsonMode)()) {
3851
+ (0, import_core9.out)(r);
3852
+ return;
3853
+ }
3854
+ (0, import_core9.info)(`${r.provider}${r.category ? `/${r.category}` : ""}${r.capability ? ` (capability: ${r.capability})` : ""}`);
3855
+ for (const [k, v] of Object.entries(r.publicConfig || {})) (0, import_core9.info)(` ${k} = ${v}`);
3856
+ for (const [k, v] of Object.entries(r.secrets || {})) (0, import_core9.info)(` ${k} = ${v} (secret)`);
3857
+ (0, import_core9.hint)("Tip: add --json for machine-readable output.");
3858
+ });
3859
+ integ.command("disconnect <provider>").description("Remove a connection.").option("--app <appId>", "app id (or selected app)").option("--category <category>", "category (for custom providers)").action(async (provider, opts) => {
3860
+ const appId = (0, import_core9.resolveApp)(opts);
3861
+ const res = await (0, import_core9.disconnectIntegration)(appId, provider, opts.category);
3862
+ if ((0, import_core9.isJsonMode)()) {
3863
+ (0, import_core9.out)(res);
3864
+ return;
3865
+ }
3866
+ (0, import_core9.info)(res.removed ? `\u2713 Disconnected ${provider}.` : `Nothing to disconnect for ${provider}.`);
3867
+ });
3868
+ }
3869
+
3870
+ // src/commands/assets.ts
3871
+ var path9 = __toESM(require("path"));
3872
+ var import_core10 = __toESM(require_dist());
3873
+ function registerAssetCommands(program2) {
3874
+ const assets = program2.command("assets").description("Manage an app's asset library (shared with the dashboard image picker).");
3875
+ assets.command("upload <file>").description("Upload a local file to the app asset library; returns the URL to embed.").option("--app <appId>", "app id (or selected app)").action(async (file, opts) => {
3876
+ const appId = (0, import_core10.resolveApp)(opts);
3877
+ const asset = await (0, import_core10.uploadAsset)(appId, file);
3878
+ if ((0, import_core10.isJsonMode)()) {
3879
+ (0, import_core10.out)(asset);
3880
+ return;
3881
+ }
3882
+ (0, import_core10.info)(`\u2713 Uploaded ${path9.basename(file)} (${asset.sizeBytes} bytes, ${asset.mimeType})`);
3883
+ (0, import_core10.out)(asset.url);
3884
+ (0, import_core10.hint)(
3885
+ `Embed in app code as: source={{ uri: '${asset.url}' }}`,
3886
+ "It now also appears in the dashboard Library tab."
3887
+ );
3888
+ });
3889
+ assets.command("list").alias("ls").description("List the app asset library.").option("--app <appId>", "app id (or selected app)").action(async (opts) => {
3890
+ const appId = (0, import_core10.resolveApp)(opts);
3891
+ const items = await (0, import_core10.listAssets)(appId);
3892
+ if ((0, import_core10.isJsonMode)()) {
3893
+ (0, import_core10.out)({ assets: items });
3894
+ return;
3895
+ }
3896
+ if (!items.length) {
3897
+ (0, import_core10.info)("(no assets)");
3898
+ (0, import_core10.hint)("Upload one: `tile assets upload <file> --app " + appId + "`");
3899
+ return;
3900
+ }
3901
+ for (const a of items) (0, import_core10.info)(` ${a.id} ${a.mimeType} ${a.sizeBytes}B ${a.url}`);
3902
+ });
3903
+ assets.command("delete <assetId>").alias("rm").description("Delete an asset by id.").option("--app <appId>", "app id (or selected app)").action(async (assetId, opts) => {
3904
+ const appId = (0, import_core10.resolveApp)(opts);
3905
+ await (0, import_core10.deleteAsset)(appId, assetId);
3906
+ if ((0, import_core10.isJsonMode)()) {
3907
+ (0, import_core10.out)({ deleted: true, id: assetId });
3908
+ return;
3909
+ }
3910
+ (0, import_core10.info)(`\u2713 Deleted ${assetId}`);
3911
+ });
3912
+ }
3913
+
3914
+ // src/commands/test.ts
3915
+ var fs7 = __toESM(require("fs"));
3916
+ var path10 = __toESM(require("path"));
3917
+ var import_child_process5 = require("child_process");
3918
+ var import_core11 = __toESM(require_dist());
3919
+ var YAML_RE = /\.ya?ml$/i;
3920
+ function probe(cmd, args) {
3921
+ const r = (0, import_child_process5.spawnSync)(cmd, args, { encoding: "utf8" });
3922
+ return { found: !r.error, status: r.status, out: `${r.stdout || ""}${r.stderr || ""}` };
3923
+ }
3924
+ function hasFlow(dir, depth = 2) {
3925
+ let entries;
3926
+ try {
3927
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
3928
+ } catch {
3929
+ return false;
3930
+ }
3931
+ for (const e of entries) {
3932
+ if (e.isFile() && YAML_RE.test(e.name)) return true;
3933
+ if (e.isDirectory() && depth > 0 && hasFlow(path10.join(dir, e.name), depth - 1)) return true;
3934
+ }
3935
+ return false;
3936
+ }
3937
+ function findFlows(root, override) {
3938
+ const candidates = override ? [override] : ["maestro", ".maestro"];
3939
+ for (const c of candidates) {
3940
+ const dir = path10.resolve(root, c);
3941
+ if (fs7.existsSync(dir) && fs7.statSync(dir).isDirectory() && hasFlow(dir)) return dir;
3942
+ }
3943
+ return null;
3944
+ }
3945
+ var SRC_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
3946
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "android", "ios", ".git", ".expo", "build", "dist", ".next"]);
3947
+ function escapeRe(s) {
3948
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3949
+ }
3950
+ function collectSource(root) {
3951
+ const files = [];
3952
+ const walk2 = (dir, depth) => {
3953
+ if (depth < 0) return;
3954
+ let entries;
3955
+ try {
3956
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
3957
+ } catch {
3958
+ return;
3959
+ }
3960
+ for (const e of entries) {
3961
+ if (e.isDirectory()) {
3962
+ if (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk2(path10.join(dir, e.name), depth - 1);
3963
+ } else if (SRC_EXTS.has(path10.extname(e.name))) {
3964
+ files.push(path10.join(dir, e.name));
3965
+ }
3966
+ }
3967
+ };
3968
+ walk2(root, 6);
3969
+ let corpus = "";
3970
+ const testIds = /* @__PURE__ */ new Set();
3971
+ const labelPatterns = [];
3972
+ const addLiteral = (s) => labelPatterns.push({ raw: s, re: new RegExp(`^${escapeRe(s)}$`) });
3973
+ const addTemplate = (tpl) => {
3974
+ const pat = tpl.replace(/\$\{[^}]*\}/g, "\0").split("\0").map(escapeRe).join(".*");
3975
+ labelPatterns.push({ raw: tpl, re: new RegExp(`^${pat}$`) });
3976
+ };
3977
+ for (const f of files) {
3978
+ let txt;
3979
+ try {
3980
+ txt = fs7.readFileSync(f, "utf8");
3981
+ } catch {
3982
+ continue;
3983
+ }
3984
+ corpus += `
3985
+ ${txt}`;
3986
+ for (const m of txt.matchAll(
3987
+ /testID\s*=\s*(?:"([^"]+)"|'([^']+)'|\{\s*(?:"([^"]+)"|'([^']+)'|`([^`]+)`)\s*\})/g
3988
+ )) {
3989
+ const v = m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5];
3990
+ if (v) testIds.add(v);
3991
+ }
3992
+ for (const m of txt.matchAll(/accessibilityLabel\s*=\s*(?:"([^"]+)"|'([^']+)')/g)) {
3993
+ const v = m[1] ?? m[2];
3994
+ if (v) addLiteral(v);
3995
+ }
3996
+ for (const m of txt.matchAll(/accessibilityLabel\s*=\s*\{\s*`([^`]+)`\s*\}/g)) addTemplate(m[1]);
3997
+ for (const m of txt.matchAll(/\bname\s*=\s*(?:"([^"]+)"|'([^']+)')/g)) {
3998
+ const v = m[1] ?? m[2];
3999
+ if (v) addLiteral(v);
4000
+ }
4001
+ }
4002
+ return { corpus, testIds, labelPatterns };
4003
+ }
4004
+ function unquote(s) {
4005
+ const t = s.trim();
4006
+ if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) return t.slice(1, -1);
4007
+ return t;
4008
+ }
4009
+ function parseFlow(text) {
4010
+ const lines = text.split("\n");
4011
+ const selectors = [];
4012
+ let commandCount = 0;
4013
+ const appId = text.match(/^appId:\s*(.+)$/m)?.[1] ? unquote(text.match(/^appId:\s*(.+)$/m)[1]) : null;
4014
+ const SELECTOR_CMDS = /^\s*-?\s*(tapOn|assertVisible|assertNotVisible|longPressOn|doubleTapOn):\s*(.*)$/;
4015
+ for (let i = 0; i < lines.length; i++) {
4016
+ const line = lines[i];
4017
+ if (/^\s*-\s+\S/.test(line)) commandCount++;
4018
+ const m = line.match(SELECTOR_CMDS);
4019
+ if (!m) continue;
4020
+ const cmd = m[1];
4021
+ const inline = m[2].trim();
4022
+ if (inline && !inline.startsWith("#")) {
4023
+ if (inline.startsWith("{")) {
4024
+ const idIn = inline.match(/id:\s*("[^"]+"|'[^']+'|[^,}\s]+)/);
4025
+ const txIn = inline.match(/text:\s*("[^"]+"|'[^']+'|[^,}]+)/);
4026
+ if (idIn) selectors.push({ kind: "id", value: unquote(idIn[1]), cmd, line: i + 1 });
4027
+ else if (txIn) selectors.push({ kind: "text", value: unquote(txIn[1]), cmd, line: i + 1 });
4028
+ } else {
4029
+ selectors.push({ kind: "text", value: unquote(inline), cmd, line: i + 1 });
4030
+ }
4031
+ continue;
4032
+ }
4033
+ const baseIndent = line.search(/\S/);
4034
+ for (let j = i + 1; j < lines.length; j++) {
4035
+ if (!lines[j].trim()) continue;
4036
+ if (lines[j].search(/\S/) <= baseIndent) break;
4037
+ const idM = lines[j].match(/^\s*id:\s*(.+)$/);
4038
+ const txM = lines[j].match(/^\s*text:\s*(.+)$/);
4039
+ if (idM) selectors.push({ kind: "id", value: unquote(idM[1]), cmd, line: j + 1 });
4040
+ else if (txM) selectors.push({ kind: "text", value: unquote(txM[1]), cmd, line: j + 1 });
4041
+ }
4042
+ }
4043
+ return { appId, commandCount, selectors };
4044
+ }
4045
+ function resolveText(value, src) {
4046
+ const core = value.replace(/^\^/, "").replace(/\$$/, "").replace(/^\.\*/, "").replace(/\.\*$/, "").trim();
4047
+ if (!core) return true;
4048
+ if (src.corpus.includes(core)) return true;
4049
+ for (const p of src.labelPatterns) if (p.re.test(core) || p.re.test(value)) return true;
4050
+ const words = core.split(/\s+/).filter((w) => w.replace(/[^A-Za-z0-9]/g, "").length > 2);
4051
+ return words.length > 0 && words.every((w) => src.corpus.includes(w));
4052
+ }
4053
+ function lintFlows(root, flowsDir) {
4054
+ const issues = [];
4055
+ const src = collectSource(root);
4056
+ const pkg = appPackage(root);
4057
+ const flowFiles = [];
4058
+ const gather = (dir) => {
4059
+ for (const e of fs7.readdirSync(dir, { withFileTypes: true })) {
4060
+ if (e.isFile() && YAML_RE.test(e.name)) flowFiles.push(path10.join(dir, e.name));
4061
+ else if (e.isDirectory()) gather(path10.join(dir, e.name));
4062
+ }
4063
+ };
4064
+ gather(flowsDir);
4065
+ for (const f of flowFiles.sort()) {
4066
+ const rel = path10.relative(root, f) || path10.basename(f);
4067
+ let text;
4068
+ try {
4069
+ text = fs7.readFileSync(f, "utf8");
4070
+ } catch {
4071
+ continue;
4072
+ }
4073
+ const { appId, commandCount, selectors } = parseFlow(text);
4074
+ if (!appId) {
4075
+ issues.push({ level: "error", flow: rel, line: 1, msg: "no `appId:` \u2014 Maestro needs it to launch the app." });
4076
+ } else if (pkg && appId !== pkg) {
4077
+ issues.push({
4078
+ level: "error",
4079
+ flow: rel,
4080
+ line: 1,
4081
+ msg: `appId "${appId}" \u2260 the app's Android applicationId "${pkg}" \u2014 the flow targets the wrong app.`
4082
+ });
4083
+ }
4084
+ if (commandCount === 0) issues.push({ level: "warn", flow: rel, line: 1, msg: "flow has no commands." });
4085
+ for (const s of selectors) {
4086
+ if (s.kind === "id") {
4087
+ if (!src.testIds.has(s.value)) {
4088
+ const looksLikeLabel = src.labelPatterns.some((p) => p.re.test(s.value));
4089
+ issues.push({
4090
+ level: "error",
4091
+ flow: rel,
4092
+ line: s.line,
4093
+ msg: looksLikeLabel ? `${s.cmd} id: "${s.value}" \u2014 that's an accessibilityLabel, not a testID. \`id:\` matches testID/resource-id only. Use \`${s.cmd}: "${s.value}"\` (text), or add testID="${s.value}" in the code.` : `${s.cmd} id: "${s.value}" \u2014 no testID="${s.value}" in the app source. \`id:\` matches a React Native testID only.`
4094
+ });
4095
+ }
4096
+ } else if (s.cmd === "tapOn" || s.cmd === "longPressOn" || s.cmd === "doubleTapOn") {
4097
+ if (!resolveText(s.value, src)) {
4098
+ issues.push({
4099
+ level: "warn",
4100
+ flow: rel,
4101
+ line: s.line,
4102
+ msg: `${s.cmd}: "${s.value}" \u2014 no matching accessibilityLabel, screen name, or text in the app source. Likely a typo or a guessed selector; tap something you explicitly labeled.`
4103
+ });
4104
+ }
4105
+ }
4106
+ }
4107
+ }
4108
+ return issues;
4109
+ }
4110
+ function printLint(issues, relFlows) {
4111
+ (0, import_core11.info)(`\u25B8 lint: ${relFlows}`);
4112
+ if (!issues.length) {
4113
+ (0, import_core11.info)(" \u2713 no issues found");
4114
+ return;
4115
+ }
4116
+ for (const i of issues) (0, import_core11.info)(` ${i.level === "error" ? "\u2717" : "\u26A0"} ${i.flow}:${i.line} \u2014 ${i.msg}`);
4117
+ }
4118
+ function preflightTools() {
4119
+ const checks = [];
4120
+ const m = probe("maestro", ["-v"]);
4121
+ checks.push({
4122
+ name: "Maestro CLI",
4123
+ ok: m.found,
4124
+ detail: m.found ? m.out.trim().split("\n")[0] || "installed" : "not found on PATH",
4125
+ fix: m.found ? void 0 : 'Install Maestro: curl -Ls "https://get.maestro.mobile.dev" | bash (then add ~/.maestro/bin to PATH)'
4126
+ });
4127
+ const javaBin = process.env.JAVA_HOME ? path10.join(process.env.JAVA_HOME, "bin", "java") : "java";
4128
+ const j = probe(javaBin, ["-version"]);
4129
+ const major = j.found ? j.out.match(/version "(\d+)/)?.[1] ?? null : null;
4130
+ const jdkOk = major === "17";
4131
+ const src = process.env.JAVA_HOME ? "JAVA_HOME" : "PATH";
4132
+ checks.push({
4133
+ name: "JDK 17",
4134
+ ok: jdkOk,
4135
+ detail: !j.found ? `java not found (${src})` : jdkOk ? `java 17 (${src})` : `java ${major ?? "?"} (${src}; need 17)`,
4136
+ fix: jdkOk ? void 0 : "Use JDK 17: export JAVA_HOME=$(/usr/libexec/java_home -v 17) (install if missing: brew install --cask temurin@17)"
4137
+ });
4138
+ const adb = probe("adb", ["version"]);
4139
+ checks.push({
4140
+ name: "Android SDK (adb)",
4141
+ ok: adb.found,
4142
+ detail: adb.found ? adb.out.trim().split("\n")[0] || "installed" : "adb not found",
4143
+ fix: adb.found ? void 0 : "Install the Android SDK + platform-tools (Android Studio), set ANDROID_HOME, and put platform-tools on PATH."
4144
+ });
4145
+ return checks;
4146
+ }
4147
+ function connectedDevices() {
4148
+ return probe("adb", ["devices"]).out.split("\n").slice(1).filter((l) => /\tdevice\s*$/.test(l)).map((l) => l.split(" ")[0]);
4149
+ }
4150
+ function listAvds() {
4151
+ return probe("emulator", ["-list-avds"]).out.trim().split("\n").filter(Boolean);
4152
+ }
4153
+ function appPackage(root) {
4154
+ try {
4155
+ const gradle = fs7.readFileSync(path10.join(root, "android", "app", "build.gradle"), "utf8");
4156
+ const id = gradle.match(/applicationId\s+['"]([^'"]+)['"]/)?.[1];
4157
+ if (id) return id;
4158
+ } catch {
4159
+ }
4160
+ try {
4161
+ const cfg = JSON.parse(fs7.readFileSync(path10.join(root, "app.json"), "utf8"));
4162
+ return cfg?.expo?.android?.package ?? null;
4163
+ } catch {
4164
+ return null;
4165
+ }
4166
+ }
4167
+ function isInstalled(pkg) {
4168
+ return probe("adb", ["shell", "pm", "list", "packages", pkg]).out.includes(`package:${pkg}`);
4169
+ }
4170
+ function installApk(apk, device) {
4171
+ (0, import_core11.info)(`\u25B8 installing ${path10.basename(apk)}\u2026`);
4172
+ const args = device ? ["-s", device, "install", "-r", apk] : ["install", "-r", apk];
4173
+ const inst = (0, import_child_process5.spawnSync)("adb", args, { stdio: "inherit" });
4174
+ if (inst.status !== 0) throw new import_core11.TileError("adb install failed.", import_core11.ExitCode.ERROR);
4175
+ }
4176
+ function buildAndInstallAndroid(root, device) {
4177
+ const androidDir = path10.join(root, "android");
4178
+ if (!fs7.existsSync(androidDir)) {
4179
+ (0, import_core11.info)("\u25B8 generating native android project (expo prebuild)\u2026");
4180
+ const pb = (0, import_child_process5.spawnSync)("npx", ["expo", "prebuild", "--platform", "android", "--no-install"], {
4181
+ stdio: "inherit",
4182
+ cwd: root
4183
+ });
4184
+ if (pb.status !== 0) throw new import_core11.TileError("expo prebuild failed.", import_core11.ExitCode.ERROR);
4185
+ }
4186
+ (0, import_core11.info)("\u25B8 building release APK (gradle assembleRelease)\u2026");
4187
+ const gradlew = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
4188
+ const g = (0, import_child_process5.spawnSync)(gradlew, [":app:assembleRelease"], { stdio: "inherit", cwd: androidDir });
4189
+ if (g.status !== 0) throw new import_core11.TileError("gradle build failed.", import_core11.ExitCode.ERROR);
4190
+ const apk = path10.join(androidDir, "app/build/outputs/apk/release/app-release.apk");
4191
+ if (!fs7.existsSync(apk)) throw new import_core11.TileError(`release APK not found at ${apk}.`, import_core11.ExitCode.ERROR);
4192
+ installApk(apk, device);
4193
+ }
4194
+ function emulatorBin() {
4195
+ const home = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
4196
+ if (home) {
4197
+ const p = path10.join(home, "emulator", "emulator");
4198
+ if (fs7.existsSync(p)) return p;
4199
+ }
4200
+ return "emulator";
4201
+ }
4202
+ function waitForBoot(timeoutMs) {
4203
+ (0, import_child_process5.spawnSync)("adb", ["wait-for-device"], { stdio: "ignore" });
4204
+ const deadline = Date.now() + timeoutMs;
4205
+ while (Date.now() < deadline) {
4206
+ if (probe("adb", ["shell", "getprop", "sys.boot_completed"]).out.trim() === "1") return true;
4207
+ (0, import_child_process5.spawnSync)("sleep", ["2"]);
4208
+ }
4209
+ return false;
4210
+ }
4211
+ function ensureDevice(opts) {
4212
+ if (opts.device || connectedDevices().length) return;
4213
+ const avds = listAvds();
4214
+ if (!avds.length) {
4215
+ throw new import_core11.TileError(
4216
+ "no device connected and no emulator (AVD) found.",
4217
+ import_core11.ExitCode.ERROR,
4218
+ "Connect a device (USB debugging) or create an AVD in Android Studio (needs a system image)."
4219
+ );
4220
+ }
4221
+ if (opts.boot === false) {
4222
+ throw new import_core11.TileError("no device connected (--no-boot set).", import_core11.ExitCode.ERROR, `Start one: emulator -avd ${avds[0]}`);
4223
+ }
4224
+ const avd = opts.avd || avds[0];
4225
+ if (!avds.includes(avd)) {
4226
+ throw new import_core11.TileError(`AVD "${avd}" not found.`, import_core11.ExitCode.ERROR, `Available: ${avds.join(", ")}`);
4227
+ }
4228
+ (0, import_core11.info)(`\u25B8 no device connected \u2014 booting emulator "${avd}" headless\u2026`);
4229
+ (0, import_child_process5.spawn)(
4230
+ emulatorBin(),
4231
+ ["-avd", avd, "-no-window", "-no-boot-anim", "-no-snapshot", "-gpu", "swiftshader_indirect"],
4232
+ { detached: true, stdio: "ignore" }
4233
+ ).unref();
4234
+ if (!waitForBoot(12e4)) {
4235
+ throw new import_core11.TileError(
4236
+ "emulator did not finish booting in time.",
4237
+ import_core11.ExitCode.ERROR,
4238
+ "Boot it manually (emulator -avd <name>) and re-run."
4239
+ );
4240
+ }
4241
+ (0, import_core11.info)(" \u2713 emulator ready");
4242
+ }
4243
+ function registerTestCommands(program2) {
4244
+ const test = program2.command("test").description(
4245
+ "Run this project's Maestro e2e flows (L3). Local-first; the same flows run on the farm via --remote."
4246
+ ).option("--local", "run in this environment (default)", true).option("--remote", "run on the Tile farm (provisions a pinned machine, runs `tile test --local` there)").option("--device <id>", "target device/emulator (adb id); default: first connected device").option("--avd <name>", "AVD to auto-boot when no device is connected (default: first available)").option("--no-boot", "do not auto-boot an emulator; fail if no device is connected").option("--apk <path>", "install + test this pre-built APK instead of building (the pipeline path \u2014 tests exactly what ships)").option("--build", "force a rebuild + install (otherwise auto-builds only when the app is not installed)").option("--platform <p>", "android (ios later)", "android").option("--flows <dir>", "flow directory (default: maestro/ then .maestro/)").option("--lint", "static-check the flows against the app source (no device/build), then exit").option("--skip-lint", "skip the automatic static pre-lint before building").action(
4247
+ async (opts) => {
4248
+ if (opts.remote) {
4249
+ throw new import_core11.TileError(
4250
+ "tile test --remote is not implemented yet (roadmap \xA73 phase 4).",
4251
+ import_core11.ExitCode.UNSUPPORTED,
4252
+ "For now run `tile test --local` (the same script the farm will call)."
4253
+ );
4254
+ }
4255
+ if (opts.platform !== "android") {
4256
+ throw new import_core11.TileError(
4257
+ `platform "${opts.platform}" is not supported yet \u2014 Android-first.`,
4258
+ import_core11.ExitCode.UNSUPPORTED,
4259
+ "iOS testing lands later on the Tart mac substrate (roadmap \xA73)."
4260
+ );
4261
+ }
4262
+ const root = process.cwd();
4263
+ const flowsDir = findFlows(root, opts.flows);
4264
+ if (!flowsDir) {
4265
+ throw new import_core11.TileError(
4266
+ "testing not configured for this project \u2014 no Maestro flows found (looked for maestro/ then .maestro/).",
4267
+ import_core11.ExitCode.UNSUPPORTED,
4268
+ "Add maestro/<flow>.yaml, or scaffold from the `base` blueprint (it ships flows + a CLAUDE.md test rule)."
4269
+ );
4270
+ }
4271
+ const relFlows = path10.relative(root, flowsDir) || flowsDir;
4272
+ if (opts.lint) {
4273
+ const issues = lintFlows(root, flowsDir);
4274
+ const errors = issues.filter((i) => i.level === "error");
4275
+ if ((0, import_core11.isJsonMode)()) (0, import_core11.out)({ ok: errors.length === 0, stage: "lint", flows: relFlows, issues });
4276
+ else {
4277
+ printLint(issues, relFlows);
4278
+ if (issues.length)
4279
+ (0, import_core11.hint)("", `${errors.length} error(s), ${issues.length - errors.length} warning(s).`);
4280
+ }
4281
+ if (errors.length) throw new import_core11.TileError(`lint failed: ${errors.length} error(s).`, import_core11.ExitCode.ERROR);
4282
+ return;
4283
+ }
4284
+ if (!opts.skipLint) {
4285
+ const issues = lintFlows(root, flowsDir);
4286
+ const errors = issues.filter((i) => i.level === "error");
4287
+ if (errors.length) {
4288
+ if ((0, import_core11.isJsonMode)()) (0, import_core11.out)({ ok: false, stage: "lint", flows: relFlows, issues });
4289
+ else {
4290
+ printLint(issues, relFlows);
4291
+ (0, import_core11.hint)("", "Fix the flow errors above and re-run, or pass --skip-lint to bypass.");
4292
+ }
4293
+ throw new import_core11.TileError(`lint failed: ${errors.length} error(s).`, import_core11.ExitCode.ERROR);
4294
+ }
4295
+ if (!(0, import_core11.isJsonMode)() && issues.length) printLint(issues, relFlows);
4296
+ }
4297
+ const checks = preflightTools();
4298
+ const failed = checks.filter((c) => !c.ok);
4299
+ if ((0, import_core11.isJsonMode)()) {
4300
+ if (failed.length) {
4301
+ (0, import_core11.out)({ ok: false, stage: "preflight", flows: relFlows, checks });
4302
+ throw new import_core11.TileError("preflight failed", import_core11.ExitCode.ERROR);
4303
+ }
4304
+ } else {
4305
+ (0, import_core11.info)(`\u25B8 flows: ${relFlows}`);
4306
+ for (const c of checks) (0, import_core11.info)(` ${c.ok ? "\u2713" : "\u2717"} ${c.name}: ${c.detail}`);
4307
+ }
4308
+ if (failed.length) {
4309
+ (0, import_core11.hint)("", "Environment not ready. Fix and re-run (Tile never auto-installs):");
4310
+ for (const c of failed) (0, import_core11.hint)(` \u2022 ${c.name} \u2014 ${c.fix}`);
4311
+ throw new import_core11.TileError(
4312
+ `preflight failed: ${failed.map((c) => c.name).join(", ")}`,
4313
+ import_core11.ExitCode.ERROR
4314
+ );
4315
+ }
4316
+ ensureDevice(opts);
4317
+ if (opts.apk) {
4318
+ if (opts.build) throw new import_core11.TileError("--apk and --build are mutually exclusive.", import_core11.ExitCode.USAGE);
4319
+ const apk = path10.resolve(opts.apk);
4320
+ if (apk.toLowerCase().endsWith(".aab")) {
4321
+ throw new import_core11.TileError(
4322
+ "an .aab cannot be installed on a device directly (Play expands it server-side).",
4323
+ import_core11.ExitCode.USAGE,
4324
+ "Make an installable APK first: bundletool build-apks --mode=universal \u2026 then pass that .apk. (Native --aab support is planned.)"
4325
+ );
4326
+ }
4327
+ if (!fs7.existsSync(apk)) throw new import_core11.TileError(`APK not found: ${apk}`, import_core11.ExitCode.NOT_FOUND);
4328
+ installApk(apk, opts.device);
4329
+ } else {
4330
+ const pkg = appPackage(root);
4331
+ const needBuild = !!opts.build || pkg !== null && !isInstalled(pkg);
4332
+ if (needBuild) {
4333
+ if (!opts.build && pkg) (0, import_core11.info)(`\u25B8 app ${pkg} not installed \u2014 building\u2026`);
4334
+ buildAndInstallAndroid(root, opts.device);
4335
+ }
4336
+ }
4337
+ const margs = [];
4338
+ if (opts.device) margs.push("--device", opts.device);
4339
+ margs.push("test", flowsDir);
4340
+ (0, import_core11.info)(`\u25B8 maestro ${margs.join(" ")}`);
4341
+ const r = (0, import_child_process5.spawnSync)("maestro", margs, { stdio: "inherit", cwd: root });
4342
+ if (r.status === 0) {
4343
+ if ((0, import_core11.isJsonMode)()) (0, import_core11.out)({ ok: true, flows: relFlows });
4344
+ else (0, import_core11.hint)("", "\u2713 tests passed.");
4345
+ return;
4346
+ }
4347
+ if ((0, import_core11.isJsonMode)()) (0, import_core11.out)({ ok: false, stage: "tests", flows: relFlows });
4348
+ throw new import_core11.TileError("tests failed.", import_core11.ExitCode.ERROR, "See the Maestro output above for the failing step.");
4349
+ }
4350
+ );
4351
+ const mark = (s) => s === "passed" ? "\u2713" : s === "failed" ? "\u2717" : "\u2013";
4352
+ test.command("run <buildId>").description("Trigger a CLOUD test run of a finished build (boots a real emulator, runs the flows).").option("--platform <p>", "android (ios later)", "android").option("--app <appId>", "app id (or tile.json / TILE_APP)").action(async (buildId, opts) => {
4353
+ const app = (0, import_core11.resolveApp)(opts);
4354
+ const { test: t } = await (0, import_core11.createTest)(app, { buildId, platform: opts.platform });
4355
+ if ((0, import_core11.isJsonMode)()) {
4356
+ (0, import_core11.out)(t);
4357
+ return;
4358
+ }
4359
+ (0, import_core11.info)(`\u2713 test queued \u2014 ${t.id} (build ${buildId})`);
4360
+ (0, import_core11.hint)(`Check the verdict: \`tile test status ${t.id}\``);
4361
+ });
4362
+ test.command("status <id>").description("Show a cloud test run\u2019s verdict (pass/fail) and what happened (lint/install/flows).").action(async (id) => {
4363
+ const { test: t, artifactsUrl } = await (0, import_core11.getTest)(id);
4364
+ if ((0, import_core11.isJsonMode)()) {
4365
+ (0, import_core11.out)({ test: t, artifactsUrl });
4366
+ return;
4367
+ }
4368
+ const verdict = t.result ? t.result === "passed" ? "\u2713 PASSED" : "\u2717 FAILED" : t.status;
4369
+ (0, import_core11.info)(`${t.id} ${verdict}`);
4370
+ if (t.buildId) (0, import_core11.info)(` build: ${t.buildId}`);
4371
+ (0, import_core11.info)(` run: ${t.status}`);
4372
+ const g = t.gates || {};
4373
+ (0, import_core11.info)(` gates: lint ${mark(g.lint)} install ${mark(g.install)} flows ${mark(g.flows)}`);
4374
+ if (t.flowsTotal != null) {
4375
+ (0, import_core11.info)(` flows: ${t.flowsTotal - (t.flowsFailed ?? 0)}/${t.flowsTotal} passed`);
4376
+ }
4377
+ if (t.error) (0, import_core11.info)(` error: ${typeof t.error === "string" ? t.error : JSON.stringify(t.error)}`);
4378
+ if (t.status === "finished" && t.result === "failed" && artifactsUrl) {
4379
+ (0, import_core11.hint)(`Failing-step screenshots + JUnit report: ${artifactsUrl}`);
4380
+ } else if (!(0, import_core11.isTerminalStatus)(t.status)) {
4381
+ (0, import_core11.hint)(`Still running \u2014 re-run \`tile test status ${id}\` in a moment.`);
4382
+ }
4383
+ });
4384
+ test.command("list [buildId]").description("List recent cloud test runs (newest first). Pass a buildId to filter to one build.").option("--app <appId>", "app id (or tile.json / TILE_APP)").option("--platform <p>", "only test runs for this platform (android|ios)").action(async (buildId, opts) => {
4385
+ const app = (0, import_core11.resolveApp)(opts);
4386
+ let tests = await (0, import_core11.listTests)(app);
4387
+ if (buildId) tests = tests.filter((t) => t.buildId === buildId);
4388
+ if (opts.platform) tests = tests.filter((t) => (t.platform ?? "android") === opts.platform);
4389
+ if ((0, import_core11.isJsonMode)()) {
4390
+ (0, import_core11.out)({ tests });
4391
+ return;
4392
+ }
4393
+ if (!tests.length) {
4394
+ (0, import_core11.info)(buildId ? `No test runs for build ${buildId}.` : "No test runs yet.");
4395
+ return;
4396
+ }
4397
+ for (const t of tests) {
4398
+ const verdict = t.result ? t.result.toUpperCase() : "-";
4399
+ (0, import_core11.info)(`${t.id} ${t.status.padEnd(12)} ${verdict.padEnd(7)} build=${t.buildId ?? "-"} ${t.createdAt ?? ""}`);
4400
+ }
4401
+ });
4402
+ test.command("cancel <id>").description("Cancel an in-progress cloud test run (tears down the runner VM).").action(async (id) => {
4403
+ const { test: t } = await (0, import_core11.cancelTest)(id);
4404
+ if ((0, import_core11.isJsonMode)()) (0, import_core11.out)(t);
4405
+ else (0, import_core11.info)(`\u2713 Cancel requested \u2014 ${t.id} is now ${t.status}.`);
4406
+ });
4407
+ }
4408
+
4409
+ // src/index.ts
4410
+ var program = new import_commander2.Command();
4411
+ program.name("tile").description("Tile \u2014 unified CLI for Tile apps (source, builds, OTA).").version(require_package().version, "-v, --version").option("--json", "machine-readable JSON output on stdout; suppresses hints").showHelpAfterError("(add --help for usage)");
4412
+ program.hook("preAction", (thisCommand) => {
4413
+ if (thisCommand.opts().json) (0, import_core12.setJsonMode)(true);
4414
+ });
4415
+ registerAuthCommands(program);
4416
+ registerLiveLayerCommands(program);
4417
+ registerBuildCommands(program);
4418
+ registerMetaCommands(program);
4419
+ registerAppCommands(program);
4420
+ registerSourceCommands(program);
4421
+ registerDevCommands(program);
4422
+ registerPreviewCommands(program);
4423
+ registerGithubCommands(program);
4424
+ registerIntegrationsCommands(program);
4425
+ registerAssetCommands(program);
4426
+ registerTestCommands(program);
4427
+ registerOtaCommands(program);
4428
+ program.action(() => {
4429
+ (0, import_core12.info)("tile \u2014 unified CLI for Tile apps (Expo/React Native).");
4430
+ (0, import_core12.info)("");
4431
+ (0, import_core12.info)(" tile guide what Tile is + the full lifecycle");
4432
+ (0, import_core12.info)(" tile status where you are + the suggested next command");
4433
+ (0, import_core12.info)(" tile register/login create an account \xB7 authenticate (or set TILE_TOKEN)");
4434
+ (0, import_core12.info)(" tile app / use create, list, select apps");
4435
+ (0, import_core12.info)(" tile init / save scaffold locally \xB7 store a new version");
4436
+ (0, import_core12.info)(" tile build ... native builds");
4437
+ (0, import_core12.info)(" tile integrations ... connect Supabase/Shopify/custom (powers the AI agent)");
4438
+ (0, import_core12.info)("");
4439
+ (0, import_core12.info)("New here? Run `tile guide`. Run `tile <command> --help` for details; add --json to parse output.");
4440
+ });
4441
+ async function main() {
4442
+ const raw = process.argv.slice(2);
4443
+ if (raw[0] === "ota") {
4444
+ const otaArgs = raw.slice(1);
4445
+ const wantsHelp = otaArgs.length === 0 || otaArgs.includes("--help") || otaArgs.includes("-h");
4446
+ if (!wantsHelp) runOtaIntercept(otaArgs);
4447
+ }
4448
+ await program.parseAsync(process.argv);
4449
+ }
4450
+ main().catch((err) => {
4451
+ if (err instanceof import_core12.TileError) {
4452
+ if ((0, import_core12.isJsonMode)()) {
4453
+ (0, import_core12.out)({ error: err.message, ...err.nextStep ? { nextStep: err.nextStep } : {} });
4454
+ } else {
4455
+ process.stderr.write(`error: ${err.message}
4456
+ `);
4457
+ if (err.nextStep) (0, import_core12.hint)(`Next: ${err.nextStep}`);
4458
+ }
4459
+ process.exit(err.exitCode);
4460
+ }
4461
+ const message = err instanceof Error ? err.message : String(err);
4462
+ if ((0, import_core12.isJsonMode)()) {
4463
+ (0, import_core12.out)({ error: message });
4464
+ } else {
4465
+ process.stderr.write(`error: ${message}
4466
+ `);
4467
+ }
4468
+ process.exit(import_core12.ExitCode.ERROR);
4469
+ });