@polterware/polter 0.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +405 -0
  3. package/dist/index.js +2935 -0
  4. package/package.json +58 -0
package/dist/index.js ADDED
@@ -0,0 +1,2935 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.tsx
4
+ import React8 from "react";
5
+ import { render } from "ink";
6
+
7
+ // src/app.tsx
8
+ import { Box as Box14, Text as Text13, useApp } from "ink";
9
+
10
+ // src/hooks/useNavigation.ts
11
+ import { useState, useCallback } from "react";
12
+ function useNavigation() {
13
+ const [state, setState] = useState({
14
+ screen: "main-menu",
15
+ params: {}
16
+ });
17
+ const navigate = useCallback((screen, params) => {
18
+ setState({
19
+ screen,
20
+ params: params ?? {}
21
+ });
22
+ }, []);
23
+ const goBack = useCallback(() => {
24
+ setState({ screen: "main-menu", params: {} });
25
+ }, []);
26
+ return { screen: state.screen, params: state.params, navigate, goBack };
27
+ }
28
+
29
+ // src/screens/MainMenu.tsx
30
+ import { useEffect as useEffect2, useMemo as useMemo2, useState as useState3 } from "react";
31
+ import { Box as Box4, Text as Text4 } from "ink";
32
+
33
+ // src/components/GhostBanner.tsx
34
+ import { Box, Text } from "ink";
35
+
36
+ // src/theme.ts
37
+ import { createRequire } from "module";
38
+ import pc from "picocolors";
39
+ var require2 = createRequire(import.meta.url);
40
+ var packageJson = require2("../package.json");
41
+ var SUPABASE_HEX = "#3ECF8E";
42
+ var SUPABASE_FG = "\x1B[38;2;62;207;142m";
43
+ var SUPABASE_BG = "\x1B[48;2;62;207;142m";
44
+ var RESET_FG = "\x1B[39m";
45
+ var RESET_BG = "\x1B[49m";
46
+ var wrapAnsi = (open, close) => (input2) => pc.isColorSupported ? `${open}${input2}${close}` : input2;
47
+ var supabase = wrapAnsi(SUPABASE_FG, RESET_FG);
48
+ var supabaseBg = wrapAnsi(SUPABASE_BG, RESET_BG);
49
+ var VERSION = packageJson.version;
50
+ var inkColors = {
51
+ accent: SUPABASE_HEX,
52
+ accentContrast: "#081116"
53
+ };
54
+ var colors = {
55
+ primary: supabase,
56
+ primaryBold: (s) => pc.bold(supabase(s)),
57
+ accent: supabase,
58
+ accentBold: (s) => pc.bold(supabase(s)),
59
+ success: supabase,
60
+ successBold: (s) => pc.bold(supabase(s)),
61
+ error: pc.red,
62
+ errorBold: (s) => pc.bold(pc.red(s)),
63
+ warning: supabase,
64
+ warningBold: (s) => pc.bold(supabase(s)),
65
+ dim: pc.dim,
66
+ bold: pc.bold,
67
+ white: pc.white,
68
+ highlight: (s) => supabaseBg(pc.black(pc.bold(s)))
69
+ };
70
+ var ghost = {
71
+ art: [
72
+ " \u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584",
73
+ " \u2588 \u2588",
74
+ " \u2588 \u2584\u2588\u2588\u2584 \u2582\u2588\u2582 \u2588",
75
+ " \u2588 \u2580\u2588\u2588\u2580 \u2594\u2588\u2594 \u2588",
76
+ " \u2588 \u2588",
77
+ " \u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588",
78
+ " \u2588 \u2588",
79
+ " \u2588\u2584\u2588\u2588\u2580\u2580\u2588\u2588\u2584\u2584\u2584\u2584\u2588\u2588\u2580\u2580\u2588\u2588\u2584\u2588"
80
+ ]
81
+ };
82
+
83
+ // src/components/GhostBanner.tsx
84
+ import { jsx, jsxs } from "react/jsx-runtime";
85
+ function GhostBanner() {
86
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", alignItems: "flex-start", gap: 2, marginBottom: 1, children: [
87
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: ghost.art.map((line, i) => /* @__PURE__ */ jsx(Text, { color: inkColors.accent, children: line }, i)) }),
88
+ /* @__PURE__ */ jsxs(
89
+ Box,
90
+ {
91
+ flexDirection: "column",
92
+ borderStyle: "single",
93
+ borderColor: inkColors.accent,
94
+ paddingX: 1,
95
+ children: [
96
+ /* @__PURE__ */ jsx(
97
+ Text,
98
+ {
99
+ backgroundColor: inkColors.accent,
100
+ color: inkColors.accentContrast,
101
+ bold: true,
102
+ children: " POLTER "
103
+ }
104
+ ),
105
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
106
+ "Version: ",
107
+ VERSION
108
+ ] }),
109
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "The modern interactive CLI for Supabase" })
110
+ ]
111
+ }
112
+ )
113
+ ] });
114
+ }
115
+
116
+ // src/components/SelectList.tsx
117
+ import { useEffect, useMemo, useState as useState2 } from "react";
118
+ import { Box as Box2, Text as Text2, useInput } from "ink";
119
+
120
+ // src/components/selectListSections.ts
121
+ function isHeader(item) {
122
+ return item?.kind === "header";
123
+ }
124
+ function buildEntryKey(item, globalIndex) {
125
+ return item.id ?? `${item.value}-${globalIndex}`;
126
+ }
127
+ function findNearestHeaderLabel(items, startIndex) {
128
+ for (let index = startIndex; index >= 0; index -= 1) {
129
+ const item = items[index];
130
+ if (isHeader(item)) {
131
+ return item?.label;
132
+ }
133
+ }
134
+ return void 0;
135
+ }
136
+ function buildBoxedSectionLayout(items, start, end) {
137
+ const visibleEntries = items.slice(start, end).map((item, offset) => ({
138
+ item,
139
+ globalIndex: start + offset
140
+ }));
141
+ const inheritedHeader = visibleEntries.length > 0 && !isHeader(visibleEntries[0]?.item) ? findNearestHeaderLabel(items, start - 1) : void 0;
142
+ const sections = [];
143
+ let currentTitle = inheritedHeader;
144
+ let currentKey = currentTitle ? `derived-${start}` : void 0;
145
+ let currentRows = [];
146
+ const flush = () => {
147
+ if (!currentTitle && currentRows.length === 0) {
148
+ return;
149
+ }
150
+ if (currentRows.length === 0) {
151
+ sections.push({
152
+ type: "heading",
153
+ key: currentKey ?? `heading-${sections.length}`,
154
+ label: currentTitle ?? ""
155
+ });
156
+ } else {
157
+ sections.push({
158
+ type: "box",
159
+ key: currentKey ?? `box-${sections.length}`,
160
+ title: currentTitle ?? "",
161
+ rows: [...currentRows]
162
+ });
163
+ }
164
+ currentTitle = void 0;
165
+ currentKey = void 0;
166
+ currentRows = [];
167
+ };
168
+ for (const entry of visibleEntries) {
169
+ if (isHeader(entry.item)) {
170
+ flush();
171
+ currentTitle = entry.item.label;
172
+ currentKey = buildEntryKey(entry.item, entry.globalIndex);
173
+ continue;
174
+ }
175
+ if (!currentTitle) {
176
+ currentTitle = "";
177
+ currentKey = buildEntryKey(entry.item, entry.globalIndex);
178
+ }
179
+ currentRows.push(entry);
180
+ }
181
+ flush();
182
+ return sections.filter(
183
+ (section) => section.type === "box" ? section.rows.length > 0 : Boolean(section.label)
184
+ );
185
+ }
186
+ function countBoxedSectionLines(sections) {
187
+ return sections.reduce((lineCount, section) => {
188
+ if (section.type === "heading") {
189
+ return lineCount + 1;
190
+ }
191
+ return lineCount + section.rows.length + (section.title ? 3 : 2);
192
+ }, 0);
193
+ }
194
+
195
+ // src/components/SelectList.tsx
196
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
197
+ function SelectList({
198
+ items,
199
+ onSelect,
200
+ onRightAction,
201
+ onCancel,
202
+ maxVisible = 16,
203
+ labelWidth = 34,
204
+ boxedSections = false
205
+ }) {
206
+ const isHeader2 = (item) => item.kind === "header";
207
+ const isSelectable = (item) => item.selectable ?? !isHeader2(item);
208
+ const isPinnedRow = (item) => item.section === "pinned-runs" || item.section === "pinned-commands";
209
+ const selectableIndexes = useMemo(
210
+ () => items.reduce((acc, item, index) => {
211
+ if (isSelectable(item)) {
212
+ acc.push(index);
213
+ }
214
+ return acc;
215
+ }, []),
216
+ [items]
217
+ );
218
+ const [selectedSelectableIndex, setSelectedSelectableIndex] = useState2(0);
219
+ useEffect(() => {
220
+ if (selectableIndexes.length === 0) {
221
+ setSelectedSelectableIndex(0);
222
+ return;
223
+ }
224
+ setSelectedSelectableIndex((prev) => {
225
+ if (prev < 0) return 0;
226
+ if (prev >= selectableIndexes.length) return selectableIndexes.length - 1;
227
+ return prev;
228
+ });
229
+ }, [selectableIndexes]);
230
+ const selectedItemIndex = selectableIndexes[selectedSelectableIndex] ?? -1;
231
+ const selectedItem = selectedItemIndex >= 0 ? items[selectedItemIndex] : void 0;
232
+ useInput((input2, key) => {
233
+ if (selectableIndexes.length === 0) {
234
+ if (key.escape && onCancel) {
235
+ onCancel();
236
+ }
237
+ return;
238
+ }
239
+ if (key.upArrow || input2 === "k") {
240
+ setSelectedSelectableIndex((prev) => {
241
+ let next = prev - 1;
242
+ if (next < 0) next = selectableIndexes.length - 1;
243
+ return next;
244
+ });
245
+ }
246
+ if (key.downArrow || input2 === "j") {
247
+ setSelectedSelectableIndex((prev) => {
248
+ let next = prev + 1;
249
+ if (next >= selectableIndexes.length) next = 0;
250
+ return next;
251
+ });
252
+ }
253
+ if (key.return) {
254
+ if (selectedItem) {
255
+ onSelect(selectedItem.value, selectedItem);
256
+ }
257
+ }
258
+ if (key.rightArrow && onRightAction && selectedItem?.rightActionable) {
259
+ onRightAction(selectedItem);
260
+ }
261
+ if (key.escape && onCancel) {
262
+ onCancel();
263
+ }
264
+ });
265
+ const getWindowStart = () => {
266
+ if (items.length <= maxVisible) {
267
+ return 0;
268
+ }
269
+ const safeSelectedIndex = selectedItemIndex >= 0 ? selectedItemIndex : 0;
270
+ let start = Math.max(
271
+ 0,
272
+ Math.min(
273
+ safeSelectedIndex - Math.floor(maxVisible / 2),
274
+ items.length - maxVisible
275
+ )
276
+ );
277
+ const firstVisible = items[start];
278
+ if (start > 0 && firstVisible && !isHeader2(firstVisible)) {
279
+ let previousHeaderIndex = -1;
280
+ for (let i = start - 1; i >= 0; i--) {
281
+ if (isHeader2(items[i])) {
282
+ previousHeaderIndex = i;
283
+ break;
284
+ }
285
+ }
286
+ if (previousHeaderIndex >= 0 && safeSelectedIndex - previousHeaderIndex < maxVisible) {
287
+ start = previousHeaderIndex;
288
+ }
289
+ }
290
+ return start;
291
+ };
292
+ const getWindowRange = () => {
293
+ const initialStart = getWindowStart();
294
+ const initialEnd = Math.min(initialStart + maxVisible, items.length);
295
+ if (!boxedSections) {
296
+ return [initialStart, initialEnd];
297
+ }
298
+ let start = initialStart;
299
+ let end = initialEnd;
300
+ while (start < end) {
301
+ const sections = buildBoxedSectionLayout(items, start, end);
302
+ if (countBoxedSectionLines(sections) <= maxVisible) {
303
+ break;
304
+ }
305
+ if (selectedItemIndex >= 0 && start === selectedItemIndex && end === selectedItemIndex + 1) {
306
+ break;
307
+ }
308
+ const canTrimStart = selectedItemIndex > start;
309
+ const canTrimEnd = selectedItemIndex < end - 1;
310
+ if (canTrimEnd && (!canTrimStart || end - selectedItemIndex >= selectedItemIndex - start)) {
311
+ end -= 1;
312
+ continue;
313
+ }
314
+ if (canTrimStart) {
315
+ start += 1;
316
+ continue;
317
+ }
318
+ end -= 1;
319
+ }
320
+ return [start, end];
321
+ };
322
+ const [windowStart, windowEnd] = getWindowRange();
323
+ const visibleItems = items.slice(windowStart, windowEnd);
324
+ const boxedLayout = boxedSections ? buildBoxedSectionLayout(items, windowStart, windowEnd) : [];
325
+ const showScrollUp = windowStart > 0;
326
+ const showScrollDown = windowEnd < items.length;
327
+ const renderSelectableRow = (item, globalIdx) => {
328
+ const isSelected = globalIdx === selectedItemIndex;
329
+ return /* @__PURE__ */ jsxs2(Box2, { gap: 1, children: [
330
+ /* @__PURE__ */ jsx2(Text2, { color: isSelected ? inkColors.accent : void 0, children: isSelected ? "\u276F" : " " }),
331
+ /* @__PURE__ */ jsx2(Box2, { width: labelWidth, children: /* @__PURE__ */ jsxs2(
332
+ Text2,
333
+ {
334
+ color: isSelected ? inkColors.accent : isPinnedRow(item) ? "white" : void 0,
335
+ bold: isSelected || isPinnedRow(item),
336
+ children: [
337
+ item.icon ? `${item.icon} ` : "",
338
+ item.label
339
+ ]
340
+ }
341
+ ) }),
342
+ item.hint && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
343
+ isSelected && item.rightActionable && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2192 pin/unpin" })
344
+ ] }, item.id ?? `${item.value}-${globalIdx}`);
345
+ };
346
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
347
+ showScrollUp && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \u2191 more" }),
348
+ boxedSections ? boxedLayout.map((section) => {
349
+ if (section.type === "heading") {
350
+ return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Text2, { color: inkColors.accent, bold: true, children: section.label }) }, section.key);
351
+ }
352
+ const hasSelectedRow = section.rows.some(
353
+ (row) => row.globalIndex === selectedItemIndex
354
+ );
355
+ const isPinnedSection = section.rows.some(
356
+ (row) => isPinnedRow(row.item)
357
+ );
358
+ return /* @__PURE__ */ jsxs2(
359
+ Box2,
360
+ {
361
+ flexDirection: "column",
362
+ borderStyle: "round",
363
+ borderColor: inkColors.accent,
364
+ borderDimColor: !hasSelectedRow && !isPinnedSection,
365
+ paddingX: 1,
366
+ children: [
367
+ section.title && /* @__PURE__ */ jsx2(Text2, { color: inkColors.accent, bold: true, children: section.title }),
368
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: section.rows.map(
369
+ (row) => renderSelectableRow(row.item, row.globalIndex)
370
+ ) })
371
+ ]
372
+ },
373
+ section.key
374
+ );
375
+ }) : visibleItems.map((item, i) => {
376
+ const globalIdx = windowStart + i;
377
+ const selectable = isSelectable(item);
378
+ if (!selectable) {
379
+ return /* @__PURE__ */ jsx2(
380
+ Box2,
381
+ {
382
+ marginTop: i === 0 ? 0 : 1,
383
+ children: /* @__PURE__ */ jsx2(Text2, { color: inkColors.accent, bold: true, children: item.label })
384
+ },
385
+ item.id ?? `${item.value}-${globalIdx}`
386
+ );
387
+ }
388
+ return renderSelectableRow(item, globalIdx);
389
+ }),
390
+ showScrollDown && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \u2193 more" })
391
+ ] });
392
+ }
393
+
394
+ // src/components/StatusBar.tsx
395
+ import { Box as Box3, Text as Text3 } from "ink";
396
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
397
+ function StatusBar({ hint }) {
398
+ return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, justifyContent: "space-between", children: [
399
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: hint || "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc back" }),
400
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
401
+ "polter v",
402
+ VERSION
403
+ ] })
404
+ ] });
405
+ }
406
+
407
+ // src/data/pins.ts
408
+ import Conf from "conf";
409
+ var config = new Conf({
410
+ projectName: process.env.NODE_ENV === "test" ? "polter-test" : "polter"
411
+ });
412
+ var COMMAND_PINS_KEY = "pinnedCommandBasesV2";
413
+ var RUN_PINS_KEY = "pinnedCommandRunsV1";
414
+ var LEGACY_PINS_KEY = "pinnedCommands";
415
+ function ensurePinsInitialized() {
416
+ if (!config.has(COMMAND_PINS_KEY)) {
417
+ config.set(COMMAND_PINS_KEY, []);
418
+ }
419
+ if (!config.has(RUN_PINS_KEY)) {
420
+ config.set(RUN_PINS_KEY, []);
421
+ }
422
+ if (config.has(LEGACY_PINS_KEY)) {
423
+ config.delete(LEGACY_PINS_KEY);
424
+ }
425
+ }
426
+ function getPinnedCommands() {
427
+ ensurePinsInitialized();
428
+ return config.get(COMMAND_PINS_KEY) || [];
429
+ }
430
+ function setPinnedCommands(commands) {
431
+ config.set(COMMAND_PINS_KEY, commands);
432
+ }
433
+ function togglePinnedCommand(cmd) {
434
+ ensurePinsInitialized();
435
+ const current = getPinnedCommands();
436
+ if (current.includes(cmd)) {
437
+ setPinnedCommands(current.filter((c) => c !== cmd));
438
+ return;
439
+ }
440
+ setPinnedCommands([cmd, ...current.filter((c) => c !== cmd)]);
441
+ }
442
+ function getPinnedRuns() {
443
+ ensurePinsInitialized();
444
+ return config.get(RUN_PINS_KEY) || [];
445
+ }
446
+ function setPinnedRuns(runs) {
447
+ config.set(RUN_PINS_KEY, runs);
448
+ }
449
+ function togglePinnedRun(runCommand2) {
450
+ ensurePinsInitialized();
451
+ const current = getPinnedRuns();
452
+ if (current.includes(runCommand2)) {
453
+ setPinnedRuns(current.filter((run) => run !== runCommand2));
454
+ return;
455
+ }
456
+ setPinnedRuns([runCommand2, ...current.filter((run) => run !== runCommand2)]);
457
+ }
458
+ function isPinnedRun(runCommand2) {
459
+ return getPinnedRuns().includes(runCommand2);
460
+ }
461
+
462
+ // src/data/commands.ts
463
+ var categories = {
464
+ "quick-start": {
465
+ icon: "\u{1F680}",
466
+ label: "Quick Start",
467
+ description: "Get started with Supabase",
468
+ commands: [
469
+ {
470
+ value: "bootstrap",
471
+ label: "bootstrap",
472
+ hint: "Bootstrap from a starter template"
473
+ },
474
+ { value: "init", label: "init", hint: "Initialize a local project" },
475
+ {
476
+ value: "login",
477
+ label: "login",
478
+ hint: "Authenticate with access token"
479
+ },
480
+ { value: "logout", label: "logout", hint: "Remove local auth token" }
481
+ ]
482
+ },
483
+ "local-dev": {
484
+ icon: "\u{1F6E0}",
485
+ label: "Local Dev",
486
+ description: "Day-to-day local workflow",
487
+ commands: [
488
+ {
489
+ value: "start",
490
+ label: "start",
491
+ hint: "Start local Supabase containers"
492
+ },
493
+ { value: "stop", label: "stop", hint: "Stop local Supabase containers" },
494
+ { value: "status", label: "status", hint: "Show container status" },
495
+ { value: "db", label: "db", hint: "Manage Postgres databases" },
496
+ {
497
+ value: "migration",
498
+ label: "migration",
499
+ hint: "Manage migration scripts"
500
+ },
501
+ { value: "seed", label: "seed", hint: "Seed from config.toml" },
502
+ { value: "test", label: "test", hint: "Run tests on local stack" }
503
+ ]
504
+ },
505
+ "inspect-generate": {
506
+ icon: "\u{1F50D}",
507
+ label: "Inspect & Generate",
508
+ description: "Tooling and introspection",
509
+ commands: [
510
+ { value: "inspect", label: "inspect", hint: "Inspect project resources" },
511
+ { value: "gen", label: "gen", hint: "Run code generation tools" },
512
+ { value: "services", label: "services", hint: "Show service versions" },
513
+ { value: "link", label: "link", hint: "Link to remote project" },
514
+ { value: "unlink", label: "unlink", hint: "Unlink remote project" }
515
+ ]
516
+ },
517
+ cloud: {
518
+ icon: "\u2601\uFE0F",
519
+ label: "Cloud Management",
520
+ description: "Core cloud resources",
521
+ commands: [
522
+ {
523
+ value: "projects",
524
+ label: "projects",
525
+ hint: "Manage Supabase projects"
526
+ },
527
+ { value: "functions", label: "functions", hint: "Manage Edge Functions" },
528
+ { value: "secrets", label: "secrets", hint: "Manage project secrets" },
529
+ {
530
+ value: "config",
531
+ label: "config",
532
+ hint: "Manage project configuration"
533
+ },
534
+ { value: "storage", label: "storage", hint: "Manage Storage objects" }
535
+ ]
536
+ },
537
+ networking: {
538
+ icon: "\u{1F310}",
539
+ label: "Networking & Security",
540
+ description: "Network and security config",
541
+ commands: [
542
+ { value: "domains", label: "domains", hint: "Manage custom domains" },
543
+ {
544
+ value: "ssl-enforcement",
545
+ label: "ssl-enforcement",
546
+ hint: "Manage SSL config"
547
+ },
548
+ {
549
+ value: "network-bans",
550
+ label: "network-bans",
551
+ hint: "Manage network bans"
552
+ },
553
+ {
554
+ value: "network-restrictions",
555
+ label: "network-restrictions",
556
+ hint: "Manage network restrictions"
557
+ },
558
+ {
559
+ value: "vanity-subdomains",
560
+ label: "vanity-subdomains",
561
+ hint: "Manage vanity subdomains"
562
+ },
563
+ {
564
+ value: "encryption",
565
+ label: "encryption",
566
+ hint: "Manage encryption keys"
567
+ }
568
+ ]
569
+ },
570
+ "org-auth": {
571
+ icon: "\u{1F465}",
572
+ label: "Organizations & Admin",
573
+ description: "Team management and admin tools",
574
+ commands: [
575
+ { value: "orgs", label: "orgs", hint: "Manage organizations" },
576
+ { value: "sso", label: "sso", hint: "Manage Single Sign-On" },
577
+ { value: "branches", label: "branches", hint: "Manage preview branches" },
578
+ { value: "backups", label: "backups", hint: "Manage physical backups" },
579
+ { value: "snippets", label: "snippets", hint: "Manage SQL snippets" },
580
+ {
581
+ value: "postgres-config",
582
+ label: "postgres-config",
583
+ hint: "Manage Postgres config"
584
+ }
585
+ ]
586
+ },
587
+ utilities: {
588
+ icon: "\u2699\uFE0F",
589
+ label: "Utilities",
590
+ description: "Help and shell completions",
591
+ commands: [
592
+ {
593
+ value: "completion",
594
+ label: "completion",
595
+ hint: "Generate shell completion script"
596
+ },
597
+ { value: "help", label: "help", hint: "Help about any command" }
598
+ ]
599
+ }
600
+ };
601
+
602
+ // src/screens/mainMenuModel.ts
603
+ var commandCatalog = Object.entries(categories).flatMap(
604
+ ([, category]) => category.commands.map((command) => ({
605
+ categoryIcon: category.icon,
606
+ categoryLabel: category.label,
607
+ value: command.value,
608
+ hint: command.hint
609
+ }))
610
+ );
611
+ var commandInfoByValue = new Map(
612
+ commandCatalog.map((entry) => [entry.value, entry])
613
+ );
614
+ function getCommandInfo(commandValue) {
615
+ return commandInfoByValue.get(commandValue);
616
+ }
617
+ function buildMainMenuItems({
618
+ pinnedCommands,
619
+ pinnedRuns
620
+ }) {
621
+ const pinnedCommandSet = new Set(pinnedCommands);
622
+ const nextItems = [];
623
+ if (pinnedRuns.length > 0) {
624
+ nextItems.push({
625
+ id: "section-pinned-runs",
626
+ value: "__section_pinned_runs__",
627
+ label: "\u25B6 Pinned Runs",
628
+ kind: "header",
629
+ selectable: false
630
+ });
631
+ for (const runCommand2 of pinnedRuns) {
632
+ const baseCommand = runCommand2.split(" ").filter(Boolean)[0] ?? "";
633
+ const info = getCommandInfo(baseCommand);
634
+ const infoHint = info ? `${info.categoryIcon} ${info.categoryLabel} \xB7 exact pinned run` : "Exact pinned run";
635
+ nextItems.push({
636
+ id: `run:${runCommand2}`,
637
+ value: runCommand2,
638
+ label: runCommand2,
639
+ hint: infoHint,
640
+ icon: "\u25B6",
641
+ kind: "run",
642
+ rightActionable: true,
643
+ section: "pinned-runs"
644
+ });
645
+ }
646
+ }
647
+ if (pinnedCommands.length > 0) {
648
+ nextItems.push({
649
+ id: "section-pinned-commands",
650
+ value: "__section_pinned_commands__",
651
+ label: "\u{1F4CC} Pinned Commands",
652
+ kind: "header",
653
+ selectable: false
654
+ });
655
+ for (const command of pinnedCommands) {
656
+ const info = getCommandInfo(command);
657
+ const infoHint = info ? `${info.categoryIcon} ${info.categoryLabel}${info.hint ? ` \xB7 ${info.hint}` : ""}` : "Pinned command";
658
+ nextItems.push({
659
+ id: `command:${command}`,
660
+ value: command,
661
+ label: command,
662
+ hint: infoHint,
663
+ icon: "\u{1F4CC}",
664
+ kind: "command",
665
+ rightActionable: true,
666
+ section: "pinned-commands"
667
+ });
668
+ }
669
+ }
670
+ nextItems.push({
671
+ id: "section-all-commands",
672
+ value: "__section_all_commands__",
673
+ label: "\u{1F4C2} All Commands",
674
+ kind: "header",
675
+ selectable: false
676
+ });
677
+ for (const [categoryKey, category] of Object.entries(categories)) {
678
+ nextItems.push({
679
+ id: `category-${categoryKey}`,
680
+ value: `__category_${categoryKey}__`,
681
+ label: `${category.icon} ${category.label}`,
682
+ kind: "header",
683
+ selectable: false
684
+ });
685
+ for (const command of category.commands) {
686
+ const pinHint = pinnedCommandSet.has(command.value) ? "pinned" : void 0;
687
+ nextItems.push({
688
+ id: `cmd-${categoryKey}-${command.value}`,
689
+ value: command.value,
690
+ label: command.label,
691
+ hint: [command.hint, pinHint].filter(Boolean).join(" \xB7 "),
692
+ kind: "command",
693
+ rightActionable: true,
694
+ section: categoryKey,
695
+ groupLabel: category.label
696
+ });
697
+ }
698
+ }
699
+ nextItems.push({
700
+ id: "section-actions",
701
+ value: "__section_actions__",
702
+ label: "\u26A1 Actions",
703
+ kind: "header",
704
+ selectable: false
705
+ });
706
+ nextItems.push({
707
+ id: "action-custom",
708
+ value: "__action_custom__",
709
+ label: "Custom Command",
710
+ hint: "Free-form args or check version",
711
+ icon: "\u270F\uFE0F",
712
+ kind: "action"
713
+ });
714
+ nextItems.push({
715
+ id: "action-update",
716
+ value: "__action_update__",
717
+ label: "Update Polter",
718
+ hint: "Update the current repo install or the global install",
719
+ icon: "\u2B06\uFE0F",
720
+ kind: "action"
721
+ });
722
+ nextItems.push({
723
+ id: "action-exit",
724
+ value: "__action_exit__",
725
+ label: "Exit",
726
+ icon: "\u{1F6AA}",
727
+ kind: "action"
728
+ });
729
+ return nextItems;
730
+ }
731
+
732
+ // src/screens/MainMenu.tsx
733
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
734
+ function MainMenu({
735
+ onNavigate,
736
+ onExit
737
+ }) {
738
+ const [pinnedCommands, setPinnedCommands2] = useState3(
739
+ () => getPinnedCommands()
740
+ );
741
+ const [pinnedRuns, setPinnedRuns2] = useState3(
742
+ () => getPinnedRuns()
743
+ );
744
+ const [pinFeedback, setPinFeedback] = useState3();
745
+ useEffect2(() => {
746
+ if (!pinFeedback) return;
747
+ const timeout = setTimeout(() => setPinFeedback(void 0), 1400);
748
+ return () => clearTimeout(timeout);
749
+ }, [pinFeedback]);
750
+ const items = useMemo2(
751
+ () => buildMainMenuItems({
752
+ pinnedCommands,
753
+ pinnedRuns
754
+ }),
755
+ [pinnedCommands, pinnedRuns]
756
+ );
757
+ const pinnedCommandSet = useMemo2(
758
+ () => new Set(pinnedCommands),
759
+ [pinnedCommands]
760
+ );
761
+ const pinnedRunSet = useMemo2(() => new Set(pinnedRuns), [pinnedRuns]);
762
+ const refreshPins = () => {
763
+ setPinnedCommands2(getPinnedCommands());
764
+ setPinnedRuns2(getPinnedRuns());
765
+ };
766
+ const handleSelect = (value, item) => {
767
+ if (!item) return;
768
+ if (item.kind === "command") {
769
+ onNavigate("command-args", { command: value });
770
+ return;
771
+ }
772
+ if (item.kind === "run") {
773
+ const args = value.split(" ").filter(Boolean);
774
+ if (args.length > 0) {
775
+ onNavigate("confirm-execute", { args });
776
+ }
777
+ return;
778
+ }
779
+ if (value === "__action_custom__") {
780
+ onNavigate("custom-command");
781
+ return;
782
+ }
783
+ if (value === "__action_update__") {
784
+ onNavigate("self-update");
785
+ return;
786
+ }
787
+ if (value === "__action_exit__") {
788
+ onExit();
789
+ }
790
+ };
791
+ const handleRightAction = (item) => {
792
+ if (item.kind === "command") {
793
+ const command = item.value;
794
+ const wasPinned = pinnedCommandSet.has(command);
795
+ togglePinnedCommand(command);
796
+ refreshPins();
797
+ setPinFeedback(
798
+ wasPinned ? `Unpinned "${command}"` : `Pinned "${command}"`
799
+ );
800
+ return;
801
+ }
802
+ if (item.kind === "run") {
803
+ const runCommand2 = item.value;
804
+ const wasPinned = pinnedRunSet.has(runCommand2);
805
+ togglePinnedRun(runCommand2);
806
+ refreshPins();
807
+ setPinFeedback(
808
+ wasPinned ? `Unpinned exact run "${runCommand2}"` : `Pinned exact run "${runCommand2}"`
809
+ );
810
+ }
811
+ };
812
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
813
+ /* @__PURE__ */ jsx4(GhostBanner, {}),
814
+ /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Choose a command, then press Enter to continue." }) }),
815
+ pinFeedback && /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: inkColors.accent, children: [
816
+ "\u2713 ",
817
+ pinFeedback
818
+ ] }) }),
819
+ /* @__PURE__ */ jsx4(
820
+ SelectList,
821
+ {
822
+ items,
823
+ onSelect: handleSelect,
824
+ onRightAction: handleRightAction,
825
+ boxedSections: true
826
+ }
827
+ ),
828
+ /* @__PURE__ */ jsx4(StatusBar, { hint: "\u2191\u2193 navigate \xB7 Enter select \xB7 \u2192 pin/unpin \xB7 Esc back" })
829
+ ] });
830
+ }
831
+
832
+ // src/screens/CommandArgs.tsx
833
+ import { useEffect as useEffect3, useMemo as useMemo3, useState as useState5 } from "react";
834
+ import { Box as Box6, Text as Text6 } from "ink";
835
+
836
+ // src/components/TextPrompt.tsx
837
+ import { useState as useState4 } from "react";
838
+ import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
839
+ import TextInputComponent from "ink-text-input";
840
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
841
+ function TextPrompt({
842
+ label,
843
+ placeholder,
844
+ onSubmit,
845
+ onCancel,
846
+ validate
847
+ }) {
848
+ const [value, setValue] = useState4("");
849
+ const [error, setError] = useState4();
850
+ useInput2((_input, key) => {
851
+ if (key.escape && onCancel) {
852
+ onCancel();
853
+ }
854
+ });
855
+ const handleSubmit = (val) => {
856
+ if (validate) {
857
+ const err = validate(val);
858
+ if (err) {
859
+ setError(err);
860
+ return;
861
+ }
862
+ }
863
+ setError(void 0);
864
+ onSubmit(val);
865
+ };
866
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
867
+ /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
868
+ /* @__PURE__ */ jsx5(Text5, { color: inkColors.accent, bold: true, children: "?" }),
869
+ /* @__PURE__ */ jsx5(Text5, { children: label })
870
+ ] }),
871
+ /* @__PURE__ */ jsxs5(Box5, { gap: 1, marginLeft: 2, children: [
872
+ /* @__PURE__ */ jsx5(Text5, { color: inkColors.accent, children: "\u276F" }),
873
+ /* @__PURE__ */ jsx5(
874
+ TextInputComponent,
875
+ {
876
+ value,
877
+ onChange: (val) => {
878
+ setValue(val);
879
+ setError(void 0);
880
+ },
881
+ onSubmit: handleSubmit,
882
+ placeholder
883
+ }
884
+ )
885
+ ] }),
886
+ error && /* @__PURE__ */ jsx5(Box5, { marginLeft: 2, children: /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
887
+ "\u2717 ",
888
+ error
889
+ ] }) })
890
+ ] });
891
+ }
892
+
893
+ // src/data/suggestedArgs.ts
894
+ var suggestedArgsByCommand = {
895
+ db: [
896
+ { value: "pull", label: "pull", hint: "Pull schema from remote", args: ["pull"] },
897
+ { value: "push", label: "push", hint: "Push local migrations", args: ["push"] },
898
+ { value: "reset", label: "reset", hint: "Reset local database", args: ["reset"] },
899
+ { value: "dump", label: "dump", hint: "Dump schema/data", args: ["dump"] },
900
+ { value: "diff", label: "diff", hint: "Show migration diff", args: ["diff"] },
901
+ { value: "lint", label: "lint", hint: "Lint SQL migrations", args: ["lint"] }
902
+ ],
903
+ migration: [
904
+ { value: "new", label: "new <name>", hint: "Create migration file", args: ["new"] },
905
+ { value: "up", label: "up", hint: "Apply pending migrations", args: ["up"] },
906
+ { value: "down", label: "down", hint: "Rollback last migration", args: ["down"] },
907
+ { value: "list", label: "list", hint: "List local migrations", args: ["list"] },
908
+ { value: "repair", label: "repair", hint: "Repair migration history", args: ["repair"] },
909
+ { value: "fetch", label: "fetch", hint: "Fetch migration history", args: ["fetch"] }
910
+ ],
911
+ projects: [
912
+ { value: "list", label: "list", hint: "List projects", args: ["list"] },
913
+ { value: "create", label: "create", hint: "Create project", args: ["create"] },
914
+ { value: "delete", label: "delete", hint: "Delete project", args: ["delete"] }
915
+ ],
916
+ functions: [
917
+ { value: "list", label: "list", hint: "List functions", args: ["list"] },
918
+ { value: "new", label: "new <name>", hint: "Create a function", args: ["new"] },
919
+ { value: "serve", label: "serve", hint: "Serve functions locally", args: ["serve"] },
920
+ { value: "deploy", label: "deploy <name>", hint: "Deploy function", args: ["deploy"] },
921
+ { value: "delete", label: "delete <name>", hint: "Delete function", args: ["delete"] }
922
+ ],
923
+ storage: [
924
+ { value: "ls", label: "ls", hint: "List storage objects", args: ["ls"] },
925
+ { value: "cp", label: "cp", hint: "Copy storage object", args: ["cp"] },
926
+ { value: "mv", label: "mv", hint: "Move storage object", args: ["mv"] },
927
+ { value: "rm", label: "rm", hint: "Remove storage object", args: ["rm"] }
928
+ ],
929
+ seed: [
930
+ { value: "run", label: "run", hint: "Run configured seed file", args: ["run"] }
931
+ ],
932
+ orgs: [
933
+ { value: "list", label: "list", hint: "List organizations", args: ["list"] },
934
+ { value: "create", label: "create", hint: "Create organization", args: ["create"] }
935
+ ],
936
+ snippets: [
937
+ { value: "list", label: "list", hint: "List SQL snippets", args: ["list"] },
938
+ { value: "create", label: "create", hint: "Create SQL snippet", args: ["create"] },
939
+ { value: "delete", label: "delete", hint: "Delete SQL snippet", args: ["delete"] }
940
+ ],
941
+ backups: [
942
+ { value: "list", label: "list", hint: "List backups", args: ["list"] },
943
+ { value: "download", label: "download", hint: "Download backup", args: ["download"] }
944
+ ]
945
+ };
946
+ function getSuggestedArgOptions(command) {
947
+ return suggestedArgsByCommand[command] ?? [];
948
+ }
949
+
950
+ // src/screens/commandArgsModel.ts
951
+ function buildRunCommand(command, extraArgs) {
952
+ return [command, ...extraArgs].join(" ");
953
+ }
954
+ function getRunCommandFromArgsSelection(command, value, suggestions) {
955
+ if (value === "__run_base__") {
956
+ return command;
957
+ }
958
+ if (!value.startsWith("suggest:")) {
959
+ return void 0;
960
+ }
961
+ const optionKey = value.replace("suggest:", "");
962
+ const option = suggestions.find((entry) => entry.value === optionKey);
963
+ if (!option) {
964
+ return void 0;
965
+ }
966
+ return buildRunCommand(command, option.args);
967
+ }
968
+ function buildCommandArgItems({
969
+ command,
970
+ suggestions,
971
+ pinnedRuns
972
+ }) {
973
+ const pinnedRunSet = new Set(pinnedRuns);
974
+ const baseRunCommand = buildRunCommand(command, []);
975
+ return [
976
+ {
977
+ value: "__suggested_header__",
978
+ label: `Suggested args for ${command}`,
979
+ kind: "header",
980
+ selectable: false
981
+ },
982
+ ...suggestions.map((option) => {
983
+ const runCommand2 = buildRunCommand(command, option.args);
984
+ const pinHint = pinnedRunSet.has(runCommand2) ? "pinned run" : void 0;
985
+ return {
986
+ value: `suggest:${option.value}`,
987
+ label: option.label,
988
+ hint: [option.hint, pinHint].filter(Boolean).join(" \xB7 "),
989
+ kind: "command",
990
+ rightActionable: true
991
+ };
992
+ }),
993
+ {
994
+ value: "__actions_header__",
995
+ label: "\u26A1 Actions",
996
+ kind: "header",
997
+ selectable: false
998
+ },
999
+ {
1000
+ value: "__run_base__",
1001
+ label: "Run without additional args",
1002
+ hint: [
1003
+ `supabase ${command}`,
1004
+ pinnedRunSet.has(baseRunCommand) ? "pinned run" : void 0
1005
+ ].filter(Boolean).join(" \xB7 "),
1006
+ kind: "action",
1007
+ rightActionable: true
1008
+ },
1009
+ {
1010
+ value: "__custom__",
1011
+ label: "Custom args...",
1012
+ hint: "Type any arguments manually",
1013
+ kind: "action"
1014
+ },
1015
+ {
1016
+ value: "__back__",
1017
+ label: "\u2190 Back to menu",
1018
+ kind: "action"
1019
+ }
1020
+ ];
1021
+ }
1022
+
1023
+ // src/screens/CommandArgs.tsx
1024
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1025
+ function CommandArgs({
1026
+ command,
1027
+ onNavigate,
1028
+ onBack
1029
+ }) {
1030
+ const suggestions = useMemo3(() => getSuggestedArgOptions(command), [command]);
1031
+ const [pinnedRuns, setPinnedRuns2] = useState5(() => getPinnedRuns());
1032
+ const [pinFeedback, setPinFeedback] = useState5();
1033
+ const [phase, setPhase] = useState5(
1034
+ suggestions.length > 0 ? "select" : "custom"
1035
+ );
1036
+ useEffect3(() => {
1037
+ setPhase(suggestions.length > 0 ? "select" : "custom");
1038
+ }, [command, suggestions.length]);
1039
+ useEffect3(() => {
1040
+ if (!pinFeedback) return;
1041
+ const timeout = setTimeout(() => setPinFeedback(void 0), 1400);
1042
+ return () => clearTimeout(timeout);
1043
+ }, [pinFeedback]);
1044
+ const navigateWithExtraArgs = (extraArgs) => {
1045
+ onNavigate("flag-selection", {
1046
+ args: [command, ...extraArgs],
1047
+ command
1048
+ });
1049
+ };
1050
+ if (!command) {
1051
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1052
+ /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: "Command not provided." }) }),
1053
+ /* @__PURE__ */ jsx6(
1054
+ SelectList,
1055
+ {
1056
+ items: [{ value: "__back__", label: "\u2190 Back to menu" }],
1057
+ onSelect: onBack,
1058
+ onCancel: onBack
1059
+ }
1060
+ )
1061
+ ] });
1062
+ }
1063
+ if (phase === "custom") {
1064
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1065
+ /* @__PURE__ */ jsxs6(Box6, { marginBottom: 1, gap: 1, children: [
1066
+ /* @__PURE__ */ jsx6(Text6, { color: inkColors.accent, bold: true, children: "Command" }),
1067
+ /* @__PURE__ */ jsx6(Text6, { children: command })
1068
+ ] }),
1069
+ /* @__PURE__ */ jsx6(
1070
+ TextPrompt,
1071
+ {
1072
+ label: `Additional args for supabase ${command}?`,
1073
+ placeholder: "e.g. list, pull, push (Enter to skip)",
1074
+ onSubmit: (extra) => {
1075
+ const extraArgs = extra.trim().split(" ").filter(Boolean);
1076
+ navigateWithExtraArgs(extraArgs);
1077
+ },
1078
+ onCancel: () => {
1079
+ if (suggestions.length > 0) {
1080
+ setPhase("select");
1081
+ return;
1082
+ }
1083
+ onBack();
1084
+ }
1085
+ }
1086
+ ),
1087
+ /* @__PURE__ */ jsx6(StatusBar, { hint: "Type args \xB7 Enter to continue \xB7 Esc to go back" })
1088
+ ] });
1089
+ }
1090
+ const items = useMemo3(
1091
+ () => buildCommandArgItems({
1092
+ command,
1093
+ suggestions,
1094
+ pinnedRuns
1095
+ }),
1096
+ [command, pinnedRuns, suggestions]
1097
+ );
1098
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1099
+ /* @__PURE__ */ jsxs6(Box6, { marginBottom: 1, gap: 1, children: [
1100
+ /* @__PURE__ */ jsx6(Text6, { color: inkColors.accent, bold: true, children: "Command" }),
1101
+ /* @__PURE__ */ jsx6(Text6, { children: command })
1102
+ ] }),
1103
+ pinFeedback && /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: inkColors.accent, children: [
1104
+ "\u2713 ",
1105
+ pinFeedback
1106
+ ] }) }),
1107
+ /* @__PURE__ */ jsx6(
1108
+ SelectList,
1109
+ {
1110
+ items,
1111
+ onSelect: (value) => {
1112
+ if (value === "__back__") {
1113
+ onBack();
1114
+ return;
1115
+ }
1116
+ if (value === "__custom__") {
1117
+ setPhase("custom");
1118
+ return;
1119
+ }
1120
+ if (value === "__run_base__") {
1121
+ navigateWithExtraArgs([]);
1122
+ return;
1123
+ }
1124
+ if (value.startsWith("suggest:")) {
1125
+ const runCommand2 = getRunCommandFromArgsSelection(
1126
+ command,
1127
+ value,
1128
+ suggestions
1129
+ );
1130
+ if (runCommand2) {
1131
+ navigateWithExtraArgs(
1132
+ runCommand2.split(" ").slice(1).filter(Boolean)
1133
+ );
1134
+ }
1135
+ return;
1136
+ }
1137
+ },
1138
+ onRightAction: (item) => {
1139
+ const runCommand2 = getRunCommandFromArgsSelection(
1140
+ command,
1141
+ item.value,
1142
+ suggestions
1143
+ );
1144
+ if (!runCommand2) {
1145
+ return;
1146
+ }
1147
+ const wasPinned = pinnedRuns.includes(runCommand2);
1148
+ togglePinnedRun(runCommand2);
1149
+ setPinnedRuns2(getPinnedRuns());
1150
+ setPinFeedback(
1151
+ wasPinned ? `Unpinned exact run "${runCommand2}"` : `Pinned exact run "${runCommand2}"`
1152
+ );
1153
+ },
1154
+ onCancel: onBack,
1155
+ boxedSections: true
1156
+ }
1157
+ ),
1158
+ /* @__PURE__ */ jsx6(StatusBar, { hint: "\u2191\u2193 navigate \xB7 Enter select \xB7 \u2192 pin run \xB7 Esc back" })
1159
+ ] });
1160
+ }
1161
+
1162
+ // src/screens/CustomCommand.tsx
1163
+ import { Box as Box7 } from "ink";
1164
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1165
+ function CustomCommand({
1166
+ onNavigate,
1167
+ onBack
1168
+ }) {
1169
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1170
+ /* @__PURE__ */ jsx7(
1171
+ TextPrompt,
1172
+ {
1173
+ label: "Enter your Supabase command/flags:",
1174
+ placeholder: "e.g. -v, status -o json, db pull",
1175
+ validate: (val) => {
1176
+ if (!val || !val.trim()) return "Please enter a command";
1177
+ return void 0;
1178
+ },
1179
+ onSubmit: (value) => {
1180
+ const args = value.split(" ").filter(Boolean);
1181
+ onNavigate("flag-selection", { args });
1182
+ },
1183
+ onCancel: onBack
1184
+ }
1185
+ ),
1186
+ /* @__PURE__ */ jsx7(StatusBar, { hint: "Type a command \xB7 Enter to continue \xB7 Esc to go back" })
1187
+ ] });
1188
+ }
1189
+
1190
+ // src/screens/FlagSelection.tsx
1191
+ import { Box as Box9 } from "ink";
1192
+
1193
+ // src/components/FlagToggle.tsx
1194
+ import { useState as useState6 } from "react";
1195
+ import { Box as Box8, Text as Text7, useInput as useInput3 } from "ink";
1196
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1197
+ function FlagToggle({
1198
+ flags,
1199
+ onSubmit,
1200
+ onCancel
1201
+ }) {
1202
+ const [cursor, setCursor] = useState6(0);
1203
+ const [selected, setSelected] = useState6(/* @__PURE__ */ new Set());
1204
+ useInput3((input2, key) => {
1205
+ if (key.upArrow || input2 === "k") {
1206
+ setCursor((prev) => prev > 0 ? prev - 1 : flags.length - 1);
1207
+ }
1208
+ if (key.downArrow || input2 === "j") {
1209
+ setCursor((prev) => prev < flags.length - 1 ? prev + 1 : 0);
1210
+ }
1211
+ if (input2 === " ") {
1212
+ setSelected((prev) => {
1213
+ const next = new Set(prev);
1214
+ const flag = flags[cursor];
1215
+ if (next.has(flag.value)) {
1216
+ next.delete(flag.value);
1217
+ } else {
1218
+ next.add(flag.value);
1219
+ }
1220
+ return next;
1221
+ });
1222
+ }
1223
+ if (key.return) {
1224
+ onSubmit(Array.from(selected));
1225
+ }
1226
+ if (key.escape && onCancel) {
1227
+ onCancel();
1228
+ }
1229
+ });
1230
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1231
+ /* @__PURE__ */ jsxs8(Box8, { marginBottom: 1, children: [
1232
+ /* @__PURE__ */ jsx8(Text7, { bold: true, color: inkColors.accent, children: "\u2691 Global Flags" }),
1233
+ /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " (Space to toggle, Enter to confirm)" })
1234
+ ] }),
1235
+ flags.map((flag, i) => {
1236
+ const isActive = cursor === i;
1237
+ const isChecked = selected.has(flag.value);
1238
+ return /* @__PURE__ */ jsxs8(Box8, { gap: 1, children: [
1239
+ /* @__PURE__ */ jsx8(Text7, { color: isActive ? inkColors.accent : void 0, children: isActive ? "\u276F" : " " }),
1240
+ /* @__PURE__ */ jsx8(
1241
+ Text7,
1242
+ {
1243
+ color: isChecked || isActive ? inkColors.accent : void 0,
1244
+ bold: isChecked,
1245
+ children: isChecked ? "\u25C9" : "\u25CB"
1246
+ }
1247
+ ),
1248
+ /* @__PURE__ */ jsx8(
1249
+ Text7,
1250
+ {
1251
+ color: isActive ? inkColors.accent : void 0,
1252
+ bold: isActive,
1253
+ children: flag.label
1254
+ }
1255
+ ),
1256
+ flag.hint && /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: flag.hint })
1257
+ ] }, flag.value);
1258
+ }),
1259
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: selected.size > 0 ? `${selected.size} flag${selected.size > 1 ? "s" : ""} selected` : "No flags selected (Enter to skip)" }) })
1260
+ ] });
1261
+ }
1262
+
1263
+ // src/data/flags.ts
1264
+ var globalFlags = [
1265
+ { value: "--debug", label: "--debug", hint: "Output debug logs to stderr" },
1266
+ { value: "--yes", label: "--yes", hint: "Answer yes to all prompts" },
1267
+ {
1268
+ value: "--create-ticket",
1269
+ label: "--create-ticket",
1270
+ hint: "Create support ticket on error"
1271
+ },
1272
+ {
1273
+ value: "--experimental",
1274
+ label: "--experimental",
1275
+ hint: "Enable experimental features"
1276
+ }
1277
+ ];
1278
+
1279
+ // src/screens/FlagSelection.tsx
1280
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1281
+ function FlagSelection({
1282
+ args,
1283
+ onNavigate,
1284
+ onBack
1285
+ }) {
1286
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1287
+ /* @__PURE__ */ jsx9(
1288
+ FlagToggle,
1289
+ {
1290
+ flags: globalFlags,
1291
+ onSubmit: (selectedFlags) => {
1292
+ const finalArgs = selectedFlags.length > 0 ? [...args, ...selectedFlags] : args;
1293
+ onNavigate("confirm-execute", { args: finalArgs });
1294
+ },
1295
+ onCancel: onBack
1296
+ }
1297
+ ),
1298
+ /* @__PURE__ */ jsx9(StatusBar, { hint: "Space toggle \xB7 Enter confirm \xB7 Esc back" })
1299
+ ] });
1300
+ }
1301
+
1302
+ // src/screens/CommandExecution.tsx
1303
+ import { useState as useState8, useEffect as useEffect4 } from "react";
1304
+ import { Box as Box12, Text as Text11 } from "ink";
1305
+
1306
+ // src/components/Spinner.tsx
1307
+ import { Text as Text8 } from "ink";
1308
+ import InkSpinner from "ink-spinner";
1309
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1310
+ function Spinner({
1311
+ label = "Running...",
1312
+ color = inkColors.accent
1313
+ }) {
1314
+ return /* @__PURE__ */ jsxs10(Text8, { children: [
1315
+ /* @__PURE__ */ jsx10(Text8, { color, children: /* @__PURE__ */ jsx10(InkSpinner, { type: "dots" }) }),
1316
+ " ",
1317
+ /* @__PURE__ */ jsx10(Text8, { children: label })
1318
+ ] });
1319
+ }
1320
+
1321
+ // src/components/ConfirmPrompt.tsx
1322
+ import { Box as Box10, Text as Text9, useInput as useInput4 } from "ink";
1323
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1324
+ function ConfirmPrompt({
1325
+ message,
1326
+ defaultValue = true,
1327
+ onConfirm
1328
+ }) {
1329
+ useInput4((input2) => {
1330
+ if (input2 === "y" || input2 === "Y") {
1331
+ onConfirm(true);
1332
+ } else if (input2 === "n" || input2 === "N") {
1333
+ onConfirm(false);
1334
+ } else if (input2 === "\r") {
1335
+ onConfirm(defaultValue);
1336
+ }
1337
+ });
1338
+ return /* @__PURE__ */ jsxs11(Box10, { gap: 1, children: [
1339
+ /* @__PURE__ */ jsx11(Text9, { color: inkColors.accent, bold: true, children: "?" }),
1340
+ /* @__PURE__ */ jsx11(Text9, { children: message }),
1341
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: defaultValue ? "(Y/n)" : "(y/N)" })
1342
+ ] });
1343
+ }
1344
+
1345
+ // src/components/Divider.tsx
1346
+ import { Text as Text10 } from "ink";
1347
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1348
+ function Divider({
1349
+ label,
1350
+ width = 50
1351
+ }) {
1352
+ if (label) {
1353
+ const labelLen = label.length + 2;
1354
+ const sideLen = Math.max(2, Math.floor((width - labelLen) / 2));
1355
+ const left = "\u2500".repeat(sideLen);
1356
+ const right = "\u2500".repeat(sideLen);
1357
+ return /* @__PURE__ */ jsxs12(Text10, { dimColor: true, children: [
1358
+ left,
1359
+ " ",
1360
+ label,
1361
+ " ",
1362
+ right
1363
+ ] });
1364
+ }
1365
+ return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "\u2500".repeat(width) });
1366
+ }
1367
+
1368
+ // src/hooks/useCommand.ts
1369
+ import { useState as useState7, useCallback as useCallback2 } from "react";
1370
+
1371
+ // src/lib/runner.ts
1372
+ import { spawn } from "child_process";
1373
+ import { existsSync } from "fs";
1374
+ import { delimiter, dirname, join, resolve } from "path";
1375
+ function getSupabaseBinaryCandidates() {
1376
+ if (process.platform === "win32") {
1377
+ return ["supabase.cmd", "supabase.exe", "supabase"];
1378
+ }
1379
+ return ["supabase"];
1380
+ }
1381
+ function hasLocalSupabaseBinary(binDir) {
1382
+ return getSupabaseBinaryCandidates().some(
1383
+ (candidate) => existsSync(join(binDir, candidate))
1384
+ );
1385
+ }
1386
+ function getPathEnvKey(env) {
1387
+ return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
1388
+ }
1389
+ function findLocalSupabaseBinDir(startDir = process.cwd()) {
1390
+ let currentDir = resolve(startDir);
1391
+ while (true) {
1392
+ const binDir = join(currentDir, "node_modules", ".bin");
1393
+ if (hasLocalSupabaseBinary(binDir)) {
1394
+ return binDir;
1395
+ }
1396
+ const parentDir = dirname(currentDir);
1397
+ if (parentDir === currentDir) {
1398
+ return void 0;
1399
+ }
1400
+ currentDir = parentDir;
1401
+ }
1402
+ }
1403
+ function resolveSupabaseCommand(startDir = process.cwd(), env = process.env) {
1404
+ const localBinDir = findLocalSupabaseBinDir(startDir);
1405
+ if (!localBinDir) {
1406
+ return {
1407
+ command: "supabase",
1408
+ env: { ...env },
1409
+ source: "path"
1410
+ };
1411
+ }
1412
+ const pathKey = getPathEnvKey(env);
1413
+ const currentPath = env[pathKey];
1414
+ return {
1415
+ command: "supabase",
1416
+ env: {
1417
+ ...env,
1418
+ [pathKey]: currentPath ? `${localBinDir}${delimiter}${currentPath}` : localBinDir
1419
+ },
1420
+ source: "repository",
1421
+ localBinDir
1422
+ };
1423
+ }
1424
+ async function runCommand(execution, args, cwd = process.cwd()) {
1425
+ return new Promise((resolve4) => {
1426
+ let stdout = "";
1427
+ let stderr = "";
1428
+ const resolvedExecution = typeof execution === "string" ? { command: execution } : execution;
1429
+ const child = spawn(resolvedExecution.command, args, {
1430
+ cwd,
1431
+ env: resolvedExecution.env,
1432
+ shell: true,
1433
+ stdio: ["inherit", "pipe", "pipe"]
1434
+ });
1435
+ child.stdout?.on("data", (data) => {
1436
+ const text = data.toString();
1437
+ stdout += text;
1438
+ process.stdout.write(text);
1439
+ });
1440
+ child.stderr?.on("data", (data) => {
1441
+ const text = data.toString();
1442
+ stderr += text;
1443
+ process.stderr.write(text);
1444
+ });
1445
+ child.on("error", (err) => {
1446
+ resolve4({
1447
+ exitCode: null,
1448
+ signal: null,
1449
+ stdout,
1450
+ stderr,
1451
+ spawnError: err.message
1452
+ });
1453
+ });
1454
+ child.on("exit", (code, signal) => {
1455
+ resolve4({ exitCode: code, signal, stdout, stderr });
1456
+ });
1457
+ });
1458
+ }
1459
+ async function runSupabaseCommand(args, cwd = process.cwd()) {
1460
+ return runCommand(resolveSupabaseCommand(cwd), args, cwd);
1461
+ }
1462
+
1463
+ // src/hooks/useCommand.ts
1464
+ function useCommand(execution = "supabase", cwd = process.cwd()) {
1465
+ const [status, setStatus] = useState7("idle");
1466
+ const [result, setResult] = useState7(null);
1467
+ const run = useCallback2(async (args) => {
1468
+ setStatus("running");
1469
+ setResult(null);
1470
+ const res = execution === "supabase" ? await runSupabaseCommand(args, cwd) : await runCommand(execution, args, cwd);
1471
+ setResult(res);
1472
+ if (res.spawnError || res.exitCode !== null && res.exitCode !== 0) {
1473
+ setStatus("error");
1474
+ } else {
1475
+ setStatus("success");
1476
+ }
1477
+ return res;
1478
+ }, [cwd, execution]);
1479
+ const reset = useCallback2(() => {
1480
+ setStatus("idle");
1481
+ setResult(null);
1482
+ }, []);
1483
+ return { status, result, run, reset };
1484
+ }
1485
+
1486
+ // src/lib/clipboard.ts
1487
+ import { spawn as spawn2, exec } from "child_process";
1488
+ async function openInBrowser(url) {
1489
+ return new Promise((resolve4) => {
1490
+ const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
1491
+ exec(cmd, () => resolve4());
1492
+ });
1493
+ }
1494
+ async function copyToClipboard(text) {
1495
+ return new Promise((resolve4) => {
1496
+ const cmd = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
1497
+ const child = spawn2(cmd, [], { shell: true });
1498
+ child.stdin?.write(text);
1499
+ child.stdin?.end();
1500
+ child.on("exit", () => resolve4());
1501
+ child.on("error", () => resolve4());
1502
+ });
1503
+ }
1504
+
1505
+ // src/screens/CommandExecution.tsx
1506
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1507
+ function CommandExecution({
1508
+ args: initialArgs,
1509
+ onBack,
1510
+ onExit
1511
+ }) {
1512
+ const [phase, setPhase] = useState8("confirm");
1513
+ const [currentArgs, setCurrentArgs] = useState8(initialArgs);
1514
+ const [pinMessage, setPinMessage] = useState8();
1515
+ const { status, result, run, reset } = useCommand();
1516
+ const cmdDisplay = `supabase ${currentArgs.join(" ")}`;
1517
+ const runCommand2 = currentArgs.join(" ");
1518
+ useEffect4(() => {
1519
+ if (phase === "running" && status === "idle") {
1520
+ run(currentArgs);
1521
+ }
1522
+ }, [phase, status, run, currentArgs]);
1523
+ useEffect4(() => {
1524
+ if (phase === "running" && status === "success") {
1525
+ if (isPinnedRun(runCommand2)) {
1526
+ setPhase("success");
1527
+ } else {
1528
+ setPhase("success-pin-offer");
1529
+ }
1530
+ }
1531
+ if (phase === "running" && status === "error") {
1532
+ setPhase("error-menu");
1533
+ }
1534
+ }, [phase, runCommand2, status]);
1535
+ if (phase === "confirm") {
1536
+ return /* @__PURE__ */ jsx13(Box12, { flexDirection: "column", children: /* @__PURE__ */ jsx13(
1537
+ ConfirmPrompt,
1538
+ {
1539
+ message: `Execute ${cmdDisplay}?`,
1540
+ defaultValue: true,
1541
+ onConfirm: (confirmed) => {
1542
+ if (confirmed) {
1543
+ setPhase("running");
1544
+ } else {
1545
+ onBack();
1546
+ }
1547
+ }
1548
+ }
1549
+ ) });
1550
+ }
1551
+ if (phase === "running") {
1552
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
1553
+ /* @__PURE__ */ jsx13(Divider, {}),
1554
+ /* @__PURE__ */ jsxs13(Box12, { marginY: 1, gap: 1, children: [
1555
+ /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, bold: true, children: "\u25B6" }),
1556
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Running:" }),
1557
+ /* @__PURE__ */ jsx13(Text11, { children: cmdDisplay })
1558
+ ] }),
1559
+ /* @__PURE__ */ jsx13(Divider, {}),
1560
+ /* @__PURE__ */ jsx13(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx13(Spinner, { label: `Executing ${cmdDisplay}...` }) })
1561
+ ] });
1562
+ }
1563
+ if (phase === "success-pin-offer") {
1564
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
1565
+ /* @__PURE__ */ jsx13(Divider, {}),
1566
+ /* @__PURE__ */ jsxs13(Box12, { marginY: 1, gap: 1, children: [
1567
+ /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, bold: true, children: "\u2713" }),
1568
+ /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, bold: true, children: "Command completed successfully!" })
1569
+ ] }),
1570
+ /* @__PURE__ */ jsx13(
1571
+ ConfirmPrompt,
1572
+ {
1573
+ message: "Pin this exact command?",
1574
+ defaultValue: false,
1575
+ onConfirm: (shouldPin) => {
1576
+ if (shouldPin && !isPinnedRun(runCommand2)) {
1577
+ togglePinnedRun(runCommand2);
1578
+ setPinMessage("Exact command pinned to Pinned Runs.");
1579
+ }
1580
+ setPhase("success");
1581
+ }
1582
+ }
1583
+ )
1584
+ ] });
1585
+ }
1586
+ if (phase === "success") {
1587
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
1588
+ /* @__PURE__ */ jsx13(Divider, {}),
1589
+ /* @__PURE__ */ jsxs13(Box12, { marginY: 1, gap: 1, children: [
1590
+ /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, bold: true, children: "\u2713" }),
1591
+ /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, bold: true, children: "Command completed successfully!" })
1592
+ ] }),
1593
+ pinMessage && /* @__PURE__ */ jsx13(Box12, { marginBottom: 1, children: /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, children: pinMessage }) }),
1594
+ /* @__PURE__ */ jsx13(
1595
+ SelectList,
1596
+ {
1597
+ items: [{ value: "__back__", label: "\u2190 Back to menu" }],
1598
+ onSelect: onBack,
1599
+ onCancel: onBack
1600
+ }
1601
+ )
1602
+ ] });
1603
+ }
1604
+ const hasDebug = currentArgs.includes("--debug");
1605
+ const errorItems = [];
1606
+ if (!result?.spawnError) {
1607
+ errorItems.push({ value: "retry", label: "\u{1F504} Retry the same command" });
1608
+ if (!hasDebug) {
1609
+ errorItems.push({
1610
+ value: "retry-debug",
1611
+ label: "\u{1F41B} Retry with --debug",
1612
+ hint: "Append --debug for verbose logs"
1613
+ });
1614
+ }
1615
+ }
1616
+ errorItems.push(
1617
+ {
1618
+ value: "docs",
1619
+ label: "\u{1F4D6} Open Supabase CLI docs",
1620
+ hint: "Opens in browser"
1621
+ },
1622
+ {
1623
+ value: "copy",
1624
+ label: "\u{1F4CB} Copy command to clipboard"
1625
+ },
1626
+ { value: "menu", label: "\u2190 Return to main menu" },
1627
+ { value: "exit", label: "\u{1F6AA} Exit Polter" }
1628
+ );
1629
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
1630
+ /* @__PURE__ */ jsx13(Divider, {}),
1631
+ result?.spawnError ? /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", marginY: 1, children: [
1632
+ /* @__PURE__ */ jsxs13(Box12, { gap: 1, children: [
1633
+ /* @__PURE__ */ jsx13(Text11, { color: "red", bold: true, children: "\u2717" }),
1634
+ /* @__PURE__ */ jsx13(Text11, { color: "red", bold: true, children: "Failed to start command" })
1635
+ ] }),
1636
+ /* @__PURE__ */ jsxs13(Box12, { marginLeft: 2, marginTop: 1, children: [
1637
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Error: " }),
1638
+ /* @__PURE__ */ jsx13(Text11, { color: "red", children: result.spawnError })
1639
+ ] }),
1640
+ (result.spawnError.includes("ENOENT") || result.spawnError.includes("not found")) && /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [
1641
+ /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, bold: true, children: "\u{1F4A1} Supabase CLI not found in this repository or PATH" }),
1642
+ /* @__PURE__ */ jsxs13(Box12, { gap: 1, children: [
1643
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Install it:" }),
1644
+ /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, children: "https://supabase.com/docs/guides/cli" })
1645
+ ] })
1646
+ ] })
1647
+ ] }) : /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", marginY: 1, children: [
1648
+ /* @__PURE__ */ jsxs13(Box12, { gap: 1, children: [
1649
+ /* @__PURE__ */ jsx13(Text11, { color: "red", bold: true, children: "\u2717" }),
1650
+ /* @__PURE__ */ jsx13(Text11, { color: "red", children: "Command failed " }),
1651
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "(exit code " }),
1652
+ /* @__PURE__ */ jsx13(Text11, { color: "red", bold: true, children: String(result?.exitCode) }),
1653
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ")" })
1654
+ ] }),
1655
+ /* @__PURE__ */ jsxs13(Box12, { marginLeft: 2, marginTop: 1, children: [
1656
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Command: " }),
1657
+ /* @__PURE__ */ jsx13(Text11, { children: cmdDisplay })
1658
+ ] }),
1659
+ !hasDebug && /* @__PURE__ */ jsxs13(Box12, { marginLeft: 2, marginTop: 1, gap: 1, children: [
1660
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "\u{1F4A1} Tip: retry with" }),
1661
+ /* @__PURE__ */ jsx13(Text11, { color: inkColors.accent, children: "--debug" }),
1662
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "to see detailed logs" })
1663
+ ] })
1664
+ ] }),
1665
+ /* @__PURE__ */ jsx13(Box12, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text11, { bold: true, children: "What would you like to do?" }) }),
1666
+ /* @__PURE__ */ jsx13(
1667
+ SelectList,
1668
+ {
1669
+ items: errorItems,
1670
+ onSelect: async (action) => {
1671
+ switch (action) {
1672
+ case "retry":
1673
+ setPinMessage(void 0);
1674
+ reset();
1675
+ setPhase("running");
1676
+ break;
1677
+ case "retry-debug": {
1678
+ const newArgs = [...currentArgs, "--debug"];
1679
+ setCurrentArgs(newArgs);
1680
+ setPinMessage(void 0);
1681
+ reset();
1682
+ setPhase("running");
1683
+ break;
1684
+ }
1685
+ case "docs":
1686
+ await openInBrowser("https://supabase.com/docs/guides/cli");
1687
+ break;
1688
+ case "copy":
1689
+ await copyToClipboard(cmdDisplay);
1690
+ break;
1691
+ case "menu":
1692
+ onBack();
1693
+ break;
1694
+ case "exit":
1695
+ onExit();
1696
+ break;
1697
+ }
1698
+ },
1699
+ onCancel: onBack
1700
+ }
1701
+ ),
1702
+ /* @__PURE__ */ jsx13(StatusBar, {})
1703
+ ] });
1704
+ }
1705
+
1706
+ // src/screens/SelfUpdate.tsx
1707
+ import { useEffect as useEffect5, useState as useState9 } from "react";
1708
+ import { Box as Box13, Text as Text12 } from "ink";
1709
+
1710
+ // src/lib/packageRoot.ts
1711
+ import { existsSync as existsSync2 } from "fs";
1712
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
1713
+ function findNearestPackageRoot(startDir = process.cwd()) {
1714
+ let currentDir = resolve2(startDir);
1715
+ while (true) {
1716
+ if (existsSync2(join2(currentDir, "package.json"))) {
1717
+ return currentDir;
1718
+ }
1719
+ const parentDir = dirname2(currentDir);
1720
+ if (parentDir === currentDir) {
1721
+ return void 0;
1722
+ }
1723
+ currentDir = parentDir;
1724
+ }
1725
+ }
1726
+
1727
+ // src/screens/SelfUpdate.tsx
1728
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1729
+ var packageName = "@polterware/polter";
1730
+ var globalUpdateArgs = ["install", "-g", `${packageName}@latest`];
1731
+ var repositoryUpdateArgs = ["install", "-D", `${packageName}@latest`];
1732
+ function getUpdateArgs(target) {
1733
+ return target === "repository" ? repositoryUpdateArgs : globalUpdateArgs;
1734
+ }
1735
+ function SelfUpdate({
1736
+ onBack,
1737
+ onExit
1738
+ }) {
1739
+ const repositoryRoot = findNearestPackageRoot();
1740
+ const [target, setTarget] = useState9(
1741
+ repositoryRoot ? "repository" : "global"
1742
+ );
1743
+ const [phase, setPhase] = useState9(
1744
+ repositoryRoot ? "target" : "confirm"
1745
+ );
1746
+ const updateArgs = getUpdateArgs(target);
1747
+ const updateDisplay = `npm ${updateArgs.join(" ")}`;
1748
+ const updateCwd = target === "repository" && repositoryRoot ? repositoryRoot : process.cwd();
1749
+ const { status, result, run, reset } = useCommand("npm", updateCwd);
1750
+ useEffect5(() => {
1751
+ if (phase === "running" && status === "idle") {
1752
+ run(updateArgs);
1753
+ }
1754
+ }, [phase, run, status, updateArgs]);
1755
+ useEffect5(() => {
1756
+ if (phase === "running" && status === "success") {
1757
+ setPhase("success");
1758
+ }
1759
+ if (phase === "running" && status === "error") {
1760
+ setPhase("error");
1761
+ }
1762
+ }, [phase, status]);
1763
+ if (phase === "target") {
1764
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
1765
+ /* @__PURE__ */ jsx14(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Text12, { bold: true, children: "Choose where to update Polter." }) }),
1766
+ /* @__PURE__ */ jsx14(
1767
+ SelectList,
1768
+ {
1769
+ items: [
1770
+ {
1771
+ value: "repository",
1772
+ label: "Current repository",
1773
+ hint: "Pin the latest version in package.json"
1774
+ },
1775
+ {
1776
+ value: "global",
1777
+ label: "Global install",
1778
+ hint: "Update the shared version available in PATH"
1779
+ },
1780
+ { value: "back", label: "\u2190 Back to menu" }
1781
+ ],
1782
+ onSelect: (value) => {
1783
+ if (value === "back") {
1784
+ onBack();
1785
+ return;
1786
+ }
1787
+ setTarget(value);
1788
+ reset();
1789
+ setPhase("confirm");
1790
+ },
1791
+ onCancel: onBack
1792
+ }
1793
+ ),
1794
+ repositoryRoot && /* @__PURE__ */ jsx14(Box13, { marginTop: 1, marginLeft: 2, children: /* @__PURE__ */ jsxs14(Text12, { dimColor: true, children: [
1795
+ "Repository root: ",
1796
+ repositoryRoot
1797
+ ] }) })
1798
+ ] });
1799
+ }
1800
+ if (phase === "confirm") {
1801
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
1802
+ /* @__PURE__ */ jsx14(
1803
+ ConfirmPrompt,
1804
+ {
1805
+ message: `Run ${updateDisplay}?`,
1806
+ defaultValue: true,
1807
+ onConfirm: (confirmed) => {
1808
+ if (confirmed) {
1809
+ reset();
1810
+ setPhase("running");
1811
+ return;
1812
+ }
1813
+ if (repositoryRoot) {
1814
+ setPhase("target");
1815
+ return;
1816
+ }
1817
+ onBack();
1818
+ }
1819
+ }
1820
+ ),
1821
+ /* @__PURE__ */ jsxs14(Box13, { marginTop: 1, marginLeft: 2, flexDirection: "column", children: [
1822
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: target === "repository" ? "This updates the dependency in the current repository." : "This updates the global npm install." }),
1823
+ target === "repository" && repositoryRoot && /* @__PURE__ */ jsxs14(Text12, { dimColor: true, children: [
1824
+ "Run location: ",
1825
+ repositoryRoot
1826
+ ] })
1827
+ ] })
1828
+ ] });
1829
+ }
1830
+ if (phase === "running") {
1831
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
1832
+ /* @__PURE__ */ jsx14(Divider, {}),
1833
+ /* @__PURE__ */ jsxs14(Box13, { marginY: 1, gap: 1, children: [
1834
+ /* @__PURE__ */ jsx14(Text12, { color: inkColors.accent, bold: true, children: "\u25B6" }),
1835
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Running:" }),
1836
+ /* @__PURE__ */ jsx14(Text12, { children: updateDisplay })
1837
+ ] }),
1838
+ /* @__PURE__ */ jsx14(Divider, {}),
1839
+ /* @__PURE__ */ jsx14(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx14(Spinner, { label: "Updating Polter..." }) })
1840
+ ] });
1841
+ }
1842
+ if (phase === "success") {
1843
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
1844
+ /* @__PURE__ */ jsx14(Divider, {}),
1845
+ /* @__PURE__ */ jsxs14(Box13, { marginY: 1, gap: 1, children: [
1846
+ /* @__PURE__ */ jsx14(Text12, { color: inkColors.accent, bold: true, children: "\u2713" }),
1847
+ /* @__PURE__ */ jsx14(Text12, { color: inkColors.accent, bold: true, children: "Update completed successfully!" })
1848
+ ] }),
1849
+ /* @__PURE__ */ jsxs14(Box13, { marginBottom: 1, marginLeft: 2, flexDirection: "column", children: [
1850
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Restart Polter to use the latest version." }),
1851
+ target === "repository" && repositoryRoot && /* @__PURE__ */ jsxs14(Text12, { dimColor: true, children: [
1852
+ "Repository updated in: ",
1853
+ repositoryRoot
1854
+ ] })
1855
+ ] }),
1856
+ /* @__PURE__ */ jsx14(
1857
+ SelectList,
1858
+ {
1859
+ items: [
1860
+ { value: "__back__", label: "\u2190 Back to menu" },
1861
+ { value: "__exit__", label: "\u{1F6AA} Exit Polter" }
1862
+ ],
1863
+ onSelect: (value) => {
1864
+ if (value === "__exit__") {
1865
+ onExit();
1866
+ return;
1867
+ }
1868
+ onBack();
1869
+ },
1870
+ onCancel: onBack
1871
+ }
1872
+ )
1873
+ ] });
1874
+ }
1875
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
1876
+ /* @__PURE__ */ jsx14(Divider, {}),
1877
+ result?.spawnError ? /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", marginY: 1, children: [
1878
+ /* @__PURE__ */ jsxs14(Box13, { gap: 1, children: [
1879
+ /* @__PURE__ */ jsx14(Text12, { color: "red", bold: true, children: "\u2717" }),
1880
+ /* @__PURE__ */ jsx14(Text12, { color: "red", bold: true, children: "Failed to start update" })
1881
+ ] }),
1882
+ /* @__PURE__ */ jsxs14(Box13, { marginLeft: 2, marginTop: 1, children: [
1883
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Error: " }),
1884
+ /* @__PURE__ */ jsx14(Text12, { color: "red", children: result.spawnError })
1885
+ ] })
1886
+ ] }) : /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", marginY: 1, children: [
1887
+ /* @__PURE__ */ jsxs14(Box13, { gap: 1, children: [
1888
+ /* @__PURE__ */ jsx14(Text12, { color: "red", bold: true, children: "\u2717" }),
1889
+ /* @__PURE__ */ jsx14(Text12, { color: "red", children: "Update failed " }),
1890
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "(exit code " }),
1891
+ /* @__PURE__ */ jsx14(Text12, { color: "red", bold: true, children: String(result?.exitCode) }),
1892
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: ")" })
1893
+ ] }),
1894
+ /* @__PURE__ */ jsxs14(Box13, { marginLeft: 2, marginTop: 1, children: [
1895
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Command: " }),
1896
+ /* @__PURE__ */ jsx14(Text12, { children: updateDisplay })
1897
+ ] })
1898
+ ] }),
1899
+ /* @__PURE__ */ jsxs14(Box13, { marginBottom: 1, marginLeft: 2, flexDirection: "column", children: [
1900
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Manual fallback:" }),
1901
+ /* @__PURE__ */ jsx14(Text12, { color: inkColors.accent, children: updateDisplay }),
1902
+ target === "repository" && repositoryRoot && /* @__PURE__ */ jsxs14(Text12, { dimColor: true, children: [
1903
+ "Run location: ",
1904
+ repositoryRoot
1905
+ ] })
1906
+ ] }),
1907
+ /* @__PURE__ */ jsx14(Box13, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx14(Text12, { bold: true, children: "What would you like to do?" }) }),
1908
+ /* @__PURE__ */ jsx14(
1909
+ SelectList,
1910
+ {
1911
+ items: [
1912
+ { value: "retry", label: "\u{1F504} Retry update" },
1913
+ ...repositoryRoot ? [{ value: "target", label: "\u2194 Choose update target" }] : [],
1914
+ { value: "menu", label: "\u2190 Return to main menu" },
1915
+ { value: "exit", label: "\u{1F6AA} Exit Polter" }
1916
+ ],
1917
+ onSelect: (value) => {
1918
+ switch (value) {
1919
+ case "retry":
1920
+ reset();
1921
+ setPhase("running");
1922
+ break;
1923
+ case "target":
1924
+ reset();
1925
+ setPhase("target");
1926
+ break;
1927
+ case "menu":
1928
+ onBack();
1929
+ break;
1930
+ case "exit":
1931
+ onExit();
1932
+ break;
1933
+ }
1934
+ },
1935
+ onCancel: onBack
1936
+ }
1937
+ ),
1938
+ /* @__PURE__ */ jsx14(StatusBar, {})
1939
+ ] });
1940
+ }
1941
+
1942
+ // src/app.tsx
1943
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1944
+ function App() {
1945
+ const { screen, params, navigate, goBack } = useNavigation();
1946
+ const { exit } = useApp();
1947
+ const handleExit = () => {
1948
+ process.stdout.write(
1949
+ "\n" + colors.dim("Thank you for using ") + colors.primaryBold("Polter") + colors.dim("!") + "\n\n"
1950
+ );
1951
+ exit();
1952
+ };
1953
+ switch (screen) {
1954
+ case "main-menu":
1955
+ return /* @__PURE__ */ jsx15(MainMenu, { onNavigate: navigate, onExit: handleExit });
1956
+ case "command-args":
1957
+ return /* @__PURE__ */ jsx15(
1958
+ CommandArgs,
1959
+ {
1960
+ command: params.command ?? "",
1961
+ onNavigate: navigate,
1962
+ onBack: goBack
1963
+ }
1964
+ );
1965
+ case "custom-command":
1966
+ return /* @__PURE__ */ jsx15(CustomCommand, { onNavigate: navigate, onBack: goBack });
1967
+ case "flag-selection":
1968
+ return /* @__PURE__ */ jsx15(
1969
+ FlagSelection,
1970
+ {
1971
+ args: params.args ?? [],
1972
+ onNavigate: navigate,
1973
+ onBack: goBack
1974
+ }
1975
+ );
1976
+ case "confirm-execute":
1977
+ case "command-execution":
1978
+ return /* @__PURE__ */ jsx15(
1979
+ CommandExecution,
1980
+ {
1981
+ args: params.args ?? [],
1982
+ onBack: goBack,
1983
+ onExit: handleExit
1984
+ }
1985
+ );
1986
+ case "self-update":
1987
+ return /* @__PURE__ */ jsx15(SelfUpdate, { onBack: goBack, onExit: handleExit });
1988
+ default:
1989
+ return /* @__PURE__ */ jsx15(Box14, { children: /* @__PURE__ */ jsxs15(Text13, { color: "red", children: [
1990
+ "Unknown screen: ",
1991
+ screen
1992
+ ] }) });
1993
+ }
1994
+ }
1995
+
1996
+ // src/lib/cliArgs.ts
1997
+ function takeValue(args, index) {
1998
+ const token = args[index];
1999
+ if (!token) {
2000
+ return { nextIndex: index };
2001
+ }
2002
+ const eqIndex = token.indexOf("=");
2003
+ if (eqIndex >= 0) {
2004
+ return {
2005
+ value: token.slice(eqIndex + 1),
2006
+ nextIndex: index
2007
+ };
2008
+ }
2009
+ return { value: args[index + 1], nextIndex: index + 1 };
2010
+ }
2011
+ function parseCliArgs(argv) {
2012
+ if (argv.length === 0) {
2013
+ return { mode: "interactive", options: {} };
2014
+ }
2015
+ if (argv[0] === "--help" || argv[0] === "help") {
2016
+ return { mode: "help", options: {} };
2017
+ }
2018
+ if (argv[0] !== "app") {
2019
+ return { mode: "interactive", options: {} };
2020
+ }
2021
+ const options = {};
2022
+ options.action = argv[1];
2023
+ options.app = argv[2];
2024
+ for (let index = 3; index < argv.length; index += 1) {
2025
+ const token = argv[index];
2026
+ if (token === "push" || token === "lint" || token === "reset" || token === "local-reset") {
2027
+ options.migrationAction = token;
2028
+ continue;
2029
+ }
2030
+ if (token === "--yes") {
2031
+ options.yes = true;
2032
+ continue;
2033
+ }
2034
+ if (token === "--relink") {
2035
+ options.relink = true;
2036
+ continue;
2037
+ }
2038
+ if (token === "--create-project") {
2039
+ options.createProject = true;
2040
+ continue;
2041
+ }
2042
+ if (token === "--use-existing-project") {
2043
+ options.useExistingProject = true;
2044
+ continue;
2045
+ }
2046
+ if (token.startsWith("--path")) {
2047
+ const parsed = takeValue(argv, index);
2048
+ options.path = parsed.value;
2049
+ index = parsed.nextIndex;
2050
+ continue;
2051
+ }
2052
+ if (token.startsWith("--version")) {
2053
+ const parsed = takeValue(argv, index);
2054
+ options.version = parsed.value;
2055
+ index = parsed.nextIndex;
2056
+ continue;
2057
+ }
2058
+ if (token.startsWith("--artifact-url")) {
2059
+ const parsed = takeValue(argv, index);
2060
+ options.artifactUrl = parsed.value;
2061
+ index = parsed.nextIndex;
2062
+ continue;
2063
+ }
2064
+ if (token.startsWith("--install-dir")) {
2065
+ const parsed = takeValue(argv, index);
2066
+ options.installDir = parsed.value;
2067
+ index = parsed.nextIndex;
2068
+ continue;
2069
+ }
2070
+ }
2071
+ return {
2072
+ mode: "app",
2073
+ options
2074
+ };
2075
+ }
2076
+ function printCliHelp() {
2077
+ process.stdout.write(
2078
+ [
2079
+ "Polter",
2080
+ "",
2081
+ "Usage:",
2082
+ " polter",
2083
+ " polter app setup ops [--path <dir>] [--create-project|--use-existing-project] [--yes]",
2084
+ " polter app link ops [--path <dir>] [--relink]",
2085
+ " polter app migrate ops [push|lint|reset|local-reset] [--path <dir>] [--relink]",
2086
+ " polter app configure ops [--path <dir>] [--yes]",
2087
+ " polter app install ops [--version <version>] [--artifact-url <url>] [--install-dir <dir>] [--yes]",
2088
+ " polter app update ops [--version <version>] [--artifact-url <url>] [--install-dir <dir>] [--yes]",
2089
+ "",
2090
+ "Notes:",
2091
+ " - App workflows stay separate from the generic Supabase interactive menu.",
2092
+ " - `install ops` is macOS-only and resolves the latest GitHub release from polterware/ops by default.",
2093
+ " - `update ops` replaces the installed app bundle without touching the persisted runtime/app state.",
2094
+ " - Use --artifact-url or POLTER_OPS_MACOS_ARTIFACT_URL to override the downloaded asset.",
2095
+ " - Use POLTER_OPS_GITHUB_REPO=owner/repo to resolve releases from a different repository.",
2096
+ ""
2097
+ ].join("\n")
2098
+ );
2099
+ }
2100
+
2101
+ // src/apps/runAppCli.ts
2102
+ import pc3 from "picocolors";
2103
+
2104
+ // src/apps/ops.ts
2105
+ import { existsSync as existsSync3, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2106
+ import { mkdtemp, readdir, stat } from "fs/promises";
2107
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
2108
+ import { tmpdir } from "os";
2109
+ import pc2 from "picocolors";
2110
+
2111
+ // src/apps/bootstrapPaths.ts
2112
+ import { homedir } from "os";
2113
+ import { join as join3 } from "path";
2114
+ function getOpsBootstrapPayloadPath() {
2115
+ const home = homedir();
2116
+ if (process.platform === "darwin") {
2117
+ return join3(
2118
+ home,
2119
+ "Library",
2120
+ "Application Support",
2121
+ "ops",
2122
+ "bootstrap",
2123
+ "supabase.json"
2124
+ );
2125
+ }
2126
+ if (process.platform === "win32") {
2127
+ const appData = process.env.APPDATA ?? join3(home, "AppData", "Roaming");
2128
+ return join3(appData, "ops", "bootstrap", "supabase.json");
2129
+ }
2130
+ return join3(home, ".config", "ops", "bootstrap", "supabase.json");
2131
+ }
2132
+
2133
+ // src/apps/opsRelease.ts
2134
+ var DEFAULT_OPS_GITHUB_REPO = "polterware/ops";
2135
+ var DEFAULT_ARTIFACT_ENV_VAR = "POLTER_OPS_MACOS_ARTIFACT_URL";
2136
+ var DEFAULT_GITHUB_REPO_ENV_VAR = "POLTER_OPS_GITHUB_REPO";
2137
+ var GITHUB_API_BASE = "https://api.github.com/repos";
2138
+ function getGitHubHeaders(env) {
2139
+ const headers = {
2140
+ Accept: "application/vnd.github+json",
2141
+ "X-GitHub-Api-Version": "2022-11-28"
2142
+ };
2143
+ const token = env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim();
2144
+ if (token) {
2145
+ headers.Authorization = `Bearer ${token}`;
2146
+ }
2147
+ return headers;
2148
+ }
2149
+ function getGitHubReleaseRequestLabel(repo, version) {
2150
+ if (!version) {
2151
+ return `latest release in ${repo}`;
2152
+ }
2153
+ return `release ${version} in ${repo}`;
2154
+ }
2155
+ function parseGitHubReleaseAsset(value) {
2156
+ if (!value || typeof value !== "object") {
2157
+ return null;
2158
+ }
2159
+ const asset = value;
2160
+ const name = typeof asset.name === "string" ? asset.name.trim() : "";
2161
+ const browserDownloadUrl = typeof asset.browser_download_url === "string" ? asset.browser_download_url.trim() : "";
2162
+ const size = typeof asset.size === "number" ? asset.size : void 0;
2163
+ if (!name || !browserDownloadUrl) {
2164
+ return null;
2165
+ }
2166
+ return {
2167
+ name,
2168
+ browserDownloadUrl,
2169
+ size
2170
+ };
2171
+ }
2172
+ async function parseGitHubReleaseResponse(response, label) {
2173
+ const body = await response.text();
2174
+ if (!response.ok) {
2175
+ let detail = body.trim();
2176
+ try {
2177
+ const parsed2 = JSON.parse(body);
2178
+ if (typeof parsed2.message === "string" && parsed2.message.trim()) {
2179
+ detail = parsed2.message.trim();
2180
+ }
2181
+ } catch {
2182
+ }
2183
+ throw new Error(
2184
+ `Unable to resolve ${label}: ${response.status} ${response.statusText}${detail ? ` (${detail})` : ""}.`
2185
+ );
2186
+ }
2187
+ let parsed;
2188
+ try {
2189
+ parsed = JSON.parse(body);
2190
+ } catch (error) {
2191
+ throw new Error(
2192
+ `Unable to parse ${label} from GitHub: ${error instanceof Error ? error.message : String(error)}.`
2193
+ );
2194
+ }
2195
+ const assets = Array.isArray(parsed.assets) ? parsed.assets.map((asset) => parseGitHubReleaseAsset(asset)).filter((asset) => asset !== null) : [];
2196
+ if (assets.length === 0) {
2197
+ throw new Error(`GitHub ${label} did not contain any downloadable assets.`);
2198
+ }
2199
+ return {
2200
+ tagName: typeof parsed.tag_name === "string" ? parsed.tag_name : null,
2201
+ assets
2202
+ };
2203
+ }
2204
+ function normalizeOpsReleaseVersion(version) {
2205
+ return version.trim().replace(/^v/i, "");
2206
+ }
2207
+ function buildVersionTagCandidates(version) {
2208
+ const normalized = normalizeOpsReleaseVersion(version);
2209
+ if (!normalized) {
2210
+ return [];
2211
+ }
2212
+ return Array.from(/* @__PURE__ */ new Set([`v${normalized}`, normalized]));
2213
+ }
2214
+ async function fetchGitHubRelease(repo, version, fetchImpl, env) {
2215
+ const headers = getGitHubHeaders(env);
2216
+ if (version) {
2217
+ const tagCandidates = buildVersionTagCandidates(version);
2218
+ for (const tag of tagCandidates) {
2219
+ const response2 = await fetchImpl(
2220
+ `${GITHUB_API_BASE}/${repo}/releases/tags/${encodeURIComponent(tag)}`,
2221
+ { headers }
2222
+ );
2223
+ if (response2.status === 404) {
2224
+ continue;
2225
+ }
2226
+ return parseGitHubReleaseResponse(
2227
+ response2,
2228
+ getGitHubReleaseRequestLabel(repo, tag)
2229
+ );
2230
+ }
2231
+ throw new Error(
2232
+ `Unable to find release ${normalizeOpsReleaseVersion(version)} in ${repo}.`
2233
+ );
2234
+ }
2235
+ const response = await fetchImpl(`${GITHUB_API_BASE}/${repo}/releases/latest`, {
2236
+ headers
2237
+ });
2238
+ return parseGitHubReleaseResponse(
2239
+ response,
2240
+ getGitHubReleaseRequestLabel(repo)
2241
+ );
2242
+ }
2243
+ function getArtifactFileNameFromUrl(url, fallback = "ops-macos.zip") {
2244
+ try {
2245
+ const pathname = new URL(url).pathname;
2246
+ const name = decodeURIComponent(pathname.split("/").pop() ?? "").trim();
2247
+ return name || fallback;
2248
+ } catch {
2249
+ const name = decodeURIComponent(url.split("?")[0]?.split("/").pop() ?? "").trim();
2250
+ return name || fallback;
2251
+ }
2252
+ }
2253
+ function hasSupportedArchiveFormat(name) {
2254
+ const lowered = name.toLowerCase();
2255
+ return lowered.endsWith(".app.tar.gz") || lowered.endsWith(".zip");
2256
+ }
2257
+ function hasMacosHint(name) {
2258
+ const lowered = name.toLowerCase();
2259
+ return lowered.endsWith(".app.tar.gz") || lowered.includes(".app.zip") || lowered.includes("macos") || lowered.includes("darwin") || lowered.includes("universal");
2260
+ }
2261
+ function isSupportedOpsMacosArtifactName(name) {
2262
+ return hasSupportedArchiveFormat(name) && hasMacosHint(name);
2263
+ }
2264
+ function getArchAliases(arch) {
2265
+ switch (arch) {
2266
+ case "arm64":
2267
+ case "aarch64":
2268
+ return ["arm64", "aarch64"];
2269
+ case "x64":
2270
+ case "x86_64":
2271
+ case "amd64":
2272
+ return ["x64", "x86_64", "amd64"];
2273
+ default:
2274
+ return [arch.toLowerCase()];
2275
+ }
2276
+ }
2277
+ function hasAnyArchHint(name) {
2278
+ return /(arm64|aarch64|x64|x86_64|amd64)/.test(name.toLowerCase());
2279
+ }
2280
+ function getArchPriority(name, arch) {
2281
+ const lowered = name.toLowerCase();
2282
+ if (lowered.includes("universal")) {
2283
+ return 0;
2284
+ }
2285
+ const aliases = getArchAliases(arch);
2286
+ if (aliases.some((alias) => lowered.includes(alias))) {
2287
+ return 1;
2288
+ }
2289
+ if (hasAnyArchHint(lowered)) {
2290
+ return 3;
2291
+ }
2292
+ return 2;
2293
+ }
2294
+ function getFormatPriority(name) {
2295
+ return name.toLowerCase().endsWith(".app.tar.gz") ? 0 : 1;
2296
+ }
2297
+ function selectOpsMacosReleaseAsset(assets, arch = process.arch) {
2298
+ const supported = assets.map((asset, index) => ({ asset, index })).filter(({ asset }) => isSupportedOpsMacosArtifactName(asset.name)).sort((left, right) => {
2299
+ const archPriority = getArchPriority(left.asset.name, arch) - getArchPriority(right.asset.name, arch);
2300
+ if (archPriority !== 0) {
2301
+ return archPriority;
2302
+ }
2303
+ const formatPriority = getFormatPriority(left.asset.name) - getFormatPriority(right.asset.name);
2304
+ if (formatPriority !== 0) {
2305
+ return formatPriority;
2306
+ }
2307
+ return left.index - right.index;
2308
+ });
2309
+ if (supported.length === 0) {
2310
+ const names = assets.map((asset) => asset.name).join(", ");
2311
+ throw new Error(
2312
+ `No supported macOS archive was found in the release. Expected a .app.tar.gz or .zip asset for macOS, found: ${names}.`
2313
+ );
2314
+ }
2315
+ return supported[0].asset;
2316
+ }
2317
+ async function resolveOpsMacosArtifact(options, env = process.env, fetchImpl = fetch, arch = process.arch) {
2318
+ const explicitUrl = options.artifactUrl?.trim() || env[DEFAULT_ARTIFACT_ENV_VAR]?.trim();
2319
+ if (explicitUrl) {
2320
+ return {
2321
+ url: explicitUrl,
2322
+ fileName: getArtifactFileNameFromUrl(explicitUrl),
2323
+ source: "explicit-url"
2324
+ };
2325
+ }
2326
+ const repo = env[DEFAULT_GITHUB_REPO_ENV_VAR]?.trim() || DEFAULT_OPS_GITHUB_REPO;
2327
+ const release = await fetchGitHubRelease(repo, options.version, fetchImpl, env);
2328
+ const asset = selectOpsMacosReleaseAsset(release.assets, arch);
2329
+ return {
2330
+ url: asset.browserDownloadUrl,
2331
+ fileName: asset.name,
2332
+ size: asset.size,
2333
+ source: "github-release",
2334
+ repo,
2335
+ tagName: release.tagName
2336
+ };
2337
+ }
2338
+
2339
+ // src/lib/prompts.ts
2340
+ import { createInterface } from "readline/promises";
2341
+ import { stdin as input, stdout as output } from "process";
2342
+ async function promptText(label, options = {}) {
2343
+ const rl = createInterface({ input, output });
2344
+ try {
2345
+ while (true) {
2346
+ const suffix = options.defaultValue ? ` (${options.defaultValue})` : "";
2347
+ const answer = (await rl.question(`${label}${suffix}: `)).trim();
2348
+ if (!answer && options.defaultValue) {
2349
+ return options.defaultValue;
2350
+ }
2351
+ if (!answer && options.required) {
2352
+ output.write("This value is required.\n");
2353
+ continue;
2354
+ }
2355
+ return answer;
2356
+ }
2357
+ } finally {
2358
+ rl.close();
2359
+ }
2360
+ }
2361
+ async function promptConfirm(label, defaultValue = true) {
2362
+ const rl = createInterface({ input, output });
2363
+ try {
2364
+ const answer = (await rl.question(`${label} ${defaultValue ? "(Y/n)" : "(y/N)"}: `)).trim().toLowerCase();
2365
+ if (!answer) {
2366
+ return defaultValue;
2367
+ }
2368
+ return answer === "y" || answer === "yes";
2369
+ } finally {
2370
+ rl.close();
2371
+ }
2372
+ }
2373
+ async function promptSelect(label, options, defaultValue) {
2374
+ const rl = createInterface({ input, output });
2375
+ try {
2376
+ output.write(`${label}
2377
+ `);
2378
+ for (const [index, option] of options.entries()) {
2379
+ const marker = option.value === defaultValue ? " (default)" : "";
2380
+ output.write(` ${index + 1}. ${option.label}${marker}
2381
+ `);
2382
+ }
2383
+ while (true) {
2384
+ const answer = (await rl.question(
2385
+ `Choose 1-${options.length}${defaultValue ? " (press Enter for default)" : ""}: `
2386
+ )).trim();
2387
+ if (!answer && defaultValue) {
2388
+ return defaultValue;
2389
+ }
2390
+ const selectedIndex = Number(answer);
2391
+ if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= options.length) {
2392
+ return options[selectedIndex - 1].value;
2393
+ }
2394
+ output.write("Invalid selection.\n");
2395
+ }
2396
+ } finally {
2397
+ rl.close();
2398
+ }
2399
+ }
2400
+
2401
+ // src/lib/system.ts
2402
+ import { execSync } from "child_process";
2403
+ function commandExists(command) {
2404
+ try {
2405
+ execSync(`command -v ${command}`, { stdio: "ignore" });
2406
+ return true;
2407
+ } catch {
2408
+ return false;
2409
+ }
2410
+ }
2411
+
2412
+ // src/apps/ops.ts
2413
+ var LINK_REF_FILE = join4("supabase", ".temp", "project-ref");
2414
+ function isOpsProjectRoot(candidate) {
2415
+ return existsSync3(join4(candidate, "src-tauri", "tauri.conf.json")) && existsSync3(join4(candidate, "supabase", "migrations")) && existsSync3(join4(candidate, "package.json"));
2416
+ }
2417
+ function findNearestOpsRoot(startDir) {
2418
+ let currentDir = resolve3(startDir);
2419
+ while (true) {
2420
+ if (isOpsProjectRoot(currentDir)) {
2421
+ return currentDir;
2422
+ }
2423
+ const siblingCandidate = join4(currentDir, "ops");
2424
+ if (isOpsProjectRoot(siblingCandidate)) {
2425
+ return siblingCandidate;
2426
+ }
2427
+ const parentDir = dirname3(currentDir);
2428
+ if (parentDir === currentDir) {
2429
+ return void 0;
2430
+ }
2431
+ currentDir = parentDir;
2432
+ }
2433
+ }
2434
+ function readEnvFile(envPath) {
2435
+ if (!existsSync3(envPath)) {
2436
+ return {};
2437
+ }
2438
+ const content = readFileSync(envPath, "utf-8");
2439
+ const entries = {};
2440
+ for (const line of content.split("\n")) {
2441
+ const trimmed = line.trim();
2442
+ if (!trimmed || trimmed.startsWith("#")) {
2443
+ continue;
2444
+ }
2445
+ const eqIndex = trimmed.indexOf("=");
2446
+ if (eqIndex === -1) {
2447
+ continue;
2448
+ }
2449
+ const key = trimmed.slice(0, eqIndex).trim();
2450
+ const value = trimmed.slice(eqIndex + 1).trim();
2451
+ entries[key] = value;
2452
+ }
2453
+ return entries;
2454
+ }
2455
+ function writeEnvFile(envPath, nextEnv) {
2456
+ const content = Object.entries(nextEnv).map(([key, value]) => `${key}=${value}`).join("\n");
2457
+ writeFileSync(envPath, `${content}
2458
+ `);
2459
+ }
2460
+ function assertProjectRoot(projectRoot) {
2461
+ if (!projectRoot) {
2462
+ throw new Error(
2463
+ "Could not resolve the Ops project root. Run from the Ops repository or pass --path."
2464
+ );
2465
+ }
2466
+ return projectRoot;
2467
+ }
2468
+ function getLinkedProjectRef(projectRoot) {
2469
+ const refPath = join4(projectRoot, LINK_REF_FILE);
2470
+ if (!existsSync3(refPath)) {
2471
+ return null;
2472
+ }
2473
+ const value = readFileSync(refPath, "utf-8").trim();
2474
+ return value || null;
2475
+ }
2476
+ function getDbPasswordArgs() {
2477
+ const password = process.env.SUPABASE_DB_PASSWORD?.trim();
2478
+ return password ? ["--password", password] : [];
2479
+ }
2480
+ async function ensurePrerequisites() {
2481
+ const checks = [
2482
+ { command: "node", label: "Node.js" },
2483
+ { command: "pnpm", label: "pnpm" },
2484
+ { command: "supabase", label: "Supabase CLI" }
2485
+ ];
2486
+ const missing = checks.filter((check) => !commandExists(check.command));
2487
+ if (missing.length > 0) {
2488
+ throw new Error(
2489
+ `Missing required tools: ${missing.map((item) => item.label).join(", ")}`
2490
+ );
2491
+ }
2492
+ }
2493
+ async function runOrThrow(execution, args, cwd, failureMessage) {
2494
+ const result = await runCommand(execution, args, cwd);
2495
+ if (result.spawnError || result.exitCode !== 0) {
2496
+ throw new Error(
2497
+ result.stderr.trim() || result.spawnError || failureMessage
2498
+ );
2499
+ }
2500
+ }
2501
+ async function runSupabaseOrThrow(args, cwd, failureMessage) {
2502
+ const result = await runSupabaseCommand(args, cwd);
2503
+ if (result.spawnError || result.exitCode !== 0) {
2504
+ throw new Error(
2505
+ result.stderr.trim() || result.spawnError || failureMessage
2506
+ );
2507
+ }
2508
+ }
2509
+ async function ensureSupabaseLink(projectRoot, forceRelink = false) {
2510
+ const linkedRef = getLinkedProjectRef(projectRoot);
2511
+ if (linkedRef && !forceRelink) {
2512
+ process.stdout.write(`${pc2.dim(`Linked project: ${linkedRef}`)}
2513
+ `);
2514
+ return;
2515
+ }
2516
+ process.stdout.write(
2517
+ `${pc2.dim(forceRelink ? "Relinking Supabase project..." : "Linking Supabase project...")}
2518
+ `
2519
+ );
2520
+ await runSupabaseOrThrow(
2521
+ ["link", ...getDbPasswordArgs()],
2522
+ projectRoot,
2523
+ "Supabase link failed."
2524
+ );
2525
+ }
2526
+ async function collectSupabaseConfig(projectRoot) {
2527
+ const envPath = projectRoot ? join4(projectRoot, ".env.local") : void 0;
2528
+ const currentEnv = envPath ? readEnvFile(envPath) : {};
2529
+ const currentRef = projectRoot ? getLinkedProjectRef(projectRoot) : null;
2530
+ const url = await promptText("Supabase URL", {
2531
+ defaultValue: currentEnv.VITE_SUPABASE_URL,
2532
+ required: true
2533
+ });
2534
+ const publishableKey = await promptText("Supabase publishable key", {
2535
+ defaultValue: currentEnv.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY,
2536
+ required: true
2537
+ });
2538
+ const projectRef = await promptText("Supabase project ref", {
2539
+ defaultValue: currentRef ?? "",
2540
+ required: true
2541
+ });
2542
+ return {
2543
+ url: url.trim().replace(/\/$/, ""),
2544
+ publishableKey: publishableKey.trim(),
2545
+ projectRef: projectRef.trim()
2546
+ };
2547
+ }
2548
+ function writeOpsEnv(projectRoot, config2) {
2549
+ const envPath = join4(projectRoot, ".env.local");
2550
+ const currentEnv = readEnvFile(envPath);
2551
+ const nextEnv = {
2552
+ ...currentEnv,
2553
+ VITE_SUPABASE_URL: config2.url,
2554
+ VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: config2.publishableKey
2555
+ };
2556
+ writeEnvFile(envPath, nextEnv);
2557
+ }
2558
+ function writeBootstrapPayload(config2) {
2559
+ const payloadPath = getOpsBootstrapPayloadPath();
2560
+ mkdirSync(dirname3(payloadPath), { recursive: true });
2561
+ writeFileSync(
2562
+ payloadPath,
2563
+ JSON.stringify(
2564
+ {
2565
+ url: config2.url,
2566
+ publishableKey: config2.publishableKey,
2567
+ projectRef: config2.projectRef,
2568
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2569
+ source: "polter"
2570
+ },
2571
+ null,
2572
+ 2
2573
+ )
2574
+ );
2575
+ return payloadPath;
2576
+ }
2577
+ async function promptProjectMode(options) {
2578
+ if (options.useExistingProject) {
2579
+ return "existing";
2580
+ }
2581
+ if (options.createProject) {
2582
+ return "create";
2583
+ }
2584
+ const selected = await promptSelect(
2585
+ "How should Polter prepare Supabase for Ops?",
2586
+ [
2587
+ { value: "existing", label: "Use an existing Supabase project" },
2588
+ { value: "create", label: "Create a new Supabase project first" }
2589
+ ],
2590
+ "existing"
2591
+ );
2592
+ return selected;
2593
+ }
2594
+ async function runSetup(context) {
2595
+ const projectRoot = assertProjectRoot(context.projectRoot);
2596
+ await ensurePrerequisites();
2597
+ const mode = await promptProjectMode(context.options);
2598
+ if (mode === "create") {
2599
+ process.stdout.write(
2600
+ `${pc2.dim("Launching interactive Supabase project creation...")}
2601
+ `
2602
+ );
2603
+ await runSupabaseOrThrow(
2604
+ ["projects", "create"],
2605
+ projectRoot,
2606
+ "Supabase project creation failed."
2607
+ );
2608
+ }
2609
+ const config2 = await collectSupabaseConfig(projectRoot);
2610
+ writeOpsEnv(projectRoot, config2);
2611
+ process.stdout.write(`${pc2.green("Saved .env.local")}
2612
+ `);
2613
+ process.stdout.write(`${pc2.dim("Installing project dependencies...")}
2614
+ `);
2615
+ await runOrThrow(
2616
+ "pnpm",
2617
+ ["install", "--frozen-lockfile"],
2618
+ projectRoot,
2619
+ "Dependency installation failed."
2620
+ );
2621
+ await ensureSupabaseLink(projectRoot, context.options.relink);
2622
+ process.stdout.write(`${pc2.dim("Pushing migrations to linked project...")}
2623
+ `);
2624
+ await runSupabaseOrThrow(
2625
+ ["db", "push", "--linked", ...getDbPasswordArgs()],
2626
+ projectRoot,
2627
+ "Migration push failed."
2628
+ );
2629
+ const payloadPath = writeBootstrapPayload(config2);
2630
+ process.stdout.write(`${pc2.green("Prepared runtime bootstrap payload")}
2631
+ `);
2632
+ process.stdout.write(`${pc2.dim(`Payload path: ${payloadPath}`)}
2633
+ `);
2634
+ return 0;
2635
+ }
2636
+ async function runLink(context) {
2637
+ const projectRoot = assertProjectRoot(context.projectRoot);
2638
+ await ensurePrerequisites();
2639
+ await ensureSupabaseLink(projectRoot, true);
2640
+ process.stdout.write(`${pc2.green("Supabase link completed")}
2641
+ `);
2642
+ return 0;
2643
+ }
2644
+ async function runMigration(context, action) {
2645
+ const projectRoot = assertProjectRoot(context.projectRoot);
2646
+ await ensurePrerequisites();
2647
+ switch (action) {
2648
+ case "local-reset":
2649
+ await runSupabaseOrThrow(
2650
+ ["db", "reset", "--local"],
2651
+ projectRoot,
2652
+ "Local reset failed."
2653
+ );
2654
+ process.stdout.write(`${pc2.green("Local Supabase reset completed")}
2655
+ `);
2656
+ return 0;
2657
+ case "push":
2658
+ await ensureSupabaseLink(projectRoot, context.options.relink);
2659
+ await runSupabaseOrThrow(
2660
+ ["db", "push", "--linked", ...getDbPasswordArgs()],
2661
+ projectRoot,
2662
+ "Migration push failed."
2663
+ );
2664
+ process.stdout.write(`${pc2.green("Migrations pushed")}
2665
+ `);
2666
+ return 0;
2667
+ case "lint":
2668
+ await ensureSupabaseLink(projectRoot, context.options.relink);
2669
+ await runSupabaseOrThrow(
2670
+ ["db", "lint", "--linked"],
2671
+ projectRoot,
2672
+ "Migration lint completed."
2673
+ );
2674
+ process.stdout.write(`${pc2.green("Migration lint completed")}
2675
+ `);
2676
+ return 0;
2677
+ case "reset":
2678
+ await ensureSupabaseLink(projectRoot, context.options.relink);
2679
+ if (!context.options.yes) {
2680
+ const confirmed = await promptConfirm(
2681
+ "This will reset the linked remote database. Continue?",
2682
+ false
2683
+ );
2684
+ if (!confirmed) {
2685
+ process.stdout.write(`${pc2.yellow("Cancelled.")}
2686
+ `);
2687
+ return 0;
2688
+ }
2689
+ }
2690
+ await runSupabaseOrThrow(
2691
+ ["db", "reset", "--linked", ...getDbPasswordArgs()],
2692
+ projectRoot,
2693
+ "Remote reset failed."
2694
+ );
2695
+ process.stdout.write(`${pc2.green("Remote reset completed")}
2696
+ `);
2697
+ return 0;
2698
+ }
2699
+ }
2700
+ async function runConfigure(context) {
2701
+ const projectRoot = context.projectRoot;
2702
+ const config2 = await collectSupabaseConfig(projectRoot);
2703
+ if (projectRoot) {
2704
+ writeOpsEnv(projectRoot, config2);
2705
+ process.stdout.write(`${pc2.green("Updated .env.local")}
2706
+ `);
2707
+ }
2708
+ const payloadPath = writeBootstrapPayload(config2);
2709
+ process.stdout.write(`${pc2.green("Updated runtime bootstrap payload")}
2710
+ `);
2711
+ process.stdout.write(`${pc2.dim(`Payload path: ${payloadPath}`)}
2712
+ `);
2713
+ return 0;
2714
+ }
2715
+ async function downloadFile(url, destinationPath) {
2716
+ const response = await fetch(url);
2717
+ if (!response.ok) {
2718
+ throw new Error(`Unable to download artifact: ${response.status} ${response.statusText}`);
2719
+ }
2720
+ const arrayBuffer = await response.arrayBuffer();
2721
+ const buffer = Buffer.from(arrayBuffer);
2722
+ writeFileSync(destinationPath, buffer);
2723
+ return buffer.byteLength;
2724
+ }
2725
+ async function extractArchive(archivePath, outputDir) {
2726
+ if (archivePath.endsWith(".zip")) {
2727
+ await runOrThrow(
2728
+ "ditto",
2729
+ ["-xk", archivePath, outputDir],
2730
+ process.cwd(),
2731
+ "Archive extraction failed."
2732
+ );
2733
+ return;
2734
+ }
2735
+ if (archivePath.endsWith(".tar.gz") || archivePath.endsWith(".tgz")) {
2736
+ await runOrThrow(
2737
+ "tar",
2738
+ ["-xzf", archivePath, "-C", outputDir],
2739
+ process.cwd(),
2740
+ "Archive extraction failed."
2741
+ );
2742
+ return;
2743
+ }
2744
+ throw new Error("Unsupported artifact format. Use a .zip or .tar.gz macOS artifact.");
2745
+ }
2746
+ async function findFirstAppBundle(dir) {
2747
+ const entries = await readdir(dir);
2748
+ for (const entry of entries) {
2749
+ const fullPath = join4(dir, entry);
2750
+ const entryStat = await stat(fullPath);
2751
+ if (entryStat.isDirectory() && entry.endsWith(".app")) {
2752
+ return fullPath;
2753
+ }
2754
+ if (entryStat.isDirectory()) {
2755
+ const nested = await findFirstAppBundle(fullPath);
2756
+ if (nested) {
2757
+ return nested;
2758
+ }
2759
+ }
2760
+ }
2761
+ return void 0;
2762
+ }
2763
+ async function deployMacosApp(context, options) {
2764
+ const artifact = await resolveOpsMacosArtifact(context.options);
2765
+ const tempRoot = await mkdtemp(join4(tmpdir(), "polter-ops-"));
2766
+ const archivePath = join4(tempRoot, artifact.fileName);
2767
+ const extractDir = join4(tempRoot, "extract");
2768
+ mkdirSync(extractDir, { recursive: true });
2769
+ if (artifact.source === "github-release") {
2770
+ const releaseLabel = artifact.tagName ?? "latest";
2771
+ process.stdout.write(
2772
+ `${pc2.dim(`Resolved ${artifact.fileName} from ${artifact.repo} (${releaseLabel})`)}
2773
+ `
2774
+ );
2775
+ } else {
2776
+ process.stdout.write(`${pc2.dim(`Using explicit artifact URL: ${artifact.url}`)}
2777
+ `);
2778
+ }
2779
+ process.stdout.write(`${pc2.dim("Downloading Ops macOS artifact...")}
2780
+ `);
2781
+ const downloadedSize = await downloadFile(artifact.url, archivePath);
2782
+ if (artifact.size && downloadedSize !== artifact.size) {
2783
+ throw new Error(
2784
+ `Downloaded file size mismatch for ${artifact.fileName}. Expected ${artifact.size} bytes but received ${downloadedSize}.`
2785
+ );
2786
+ }
2787
+ process.stdout.write(`${pc2.dim("Extracting artifact...")}
2788
+ `);
2789
+ await extractArchive(archivePath, extractDir);
2790
+ const appBundle = await findFirstAppBundle(extractDir);
2791
+ if (!appBundle) {
2792
+ throw new Error("No .app bundle was found inside the downloaded artifact.");
2793
+ }
2794
+ const installDir = context.options.installDir ?? "/Applications";
2795
+ mkdirSync(installDir, { recursive: true });
2796
+ const destination = join4(installDir, "ops.app");
2797
+ if (options.requireExistingInstallation && !existsSync3(destination)) {
2798
+ throw new Error(
2799
+ `No existing Ops installation was found at ${destination}. Run \`polter app install ops\` first.`
2800
+ );
2801
+ }
2802
+ if (existsSync3(destination)) {
2803
+ const confirmed = context.options.yes || await promptConfirm(`Replace existing installation at ${destination}?`, false);
2804
+ if (!confirmed) {
2805
+ process.stdout.write(`${pc2.yellow("Cancelled.")}
2806
+ `);
2807
+ return 0;
2808
+ }
2809
+ rmSync(destination, { recursive: true, force: true });
2810
+ }
2811
+ await runOrThrow(
2812
+ "cp",
2813
+ ["-R", appBundle, destination],
2814
+ process.cwd(),
2815
+ "App copy failed."
2816
+ );
2817
+ process.stdout.write(
2818
+ `${pc2.green(`${options.mode === "install" ? "Installed" : "Updated"} Ops at ${destination}`)}
2819
+ `
2820
+ );
2821
+ if (options.configureAfterCopy) {
2822
+ await runConfigure(context);
2823
+ } else {
2824
+ process.stdout.write(
2825
+ `${pc2.dim("Preserved existing runtime configuration and local app state.")}
2826
+ `
2827
+ );
2828
+ }
2829
+ const shouldOpen = context.options.yes || await promptConfirm("Open Ops now?", true);
2830
+ if (shouldOpen) {
2831
+ await runOrThrow("open", ["-a", destination], process.cwd(), "Unable to open Ops.");
2832
+ }
2833
+ return 0;
2834
+ }
2835
+ async function installMacosApp(context) {
2836
+ return deployMacosApp(context, {
2837
+ mode: "install",
2838
+ configureAfterCopy: true,
2839
+ requireExistingInstallation: false
2840
+ });
2841
+ }
2842
+ async function updateMacosApp(context) {
2843
+ return deployMacosApp(context, {
2844
+ mode: "update",
2845
+ configureAfterCopy: false,
2846
+ requireExistingInstallation: true
2847
+ });
2848
+ }
2849
+ var opsProfile = {
2850
+ id: "ops",
2851
+ displayName: "Ops",
2852
+ detect(startDir = process.cwd()) {
2853
+ return findNearestOpsRoot(startDir);
2854
+ },
2855
+ resolveProjectRoot(startDir = process.cwd(), explicitPath) {
2856
+ if (explicitPath) {
2857
+ const resolved = resolve3(explicitPath);
2858
+ if (isOpsProjectRoot(resolved)) {
2859
+ return resolved;
2860
+ }
2861
+ return findNearestOpsRoot(resolved);
2862
+ }
2863
+ return findNearestOpsRoot(startDir);
2864
+ },
2865
+ async run(action, context) {
2866
+ switch (action) {
2867
+ case "setup":
2868
+ return runSetup(context);
2869
+ case "link":
2870
+ return runLink(context);
2871
+ case "configure":
2872
+ return runConfigure(context);
2873
+ case "install":
2874
+ return installMacosApp(context);
2875
+ case "update":
2876
+ return updateMacosApp(context);
2877
+ case "migrate":
2878
+ return runMigration(
2879
+ context,
2880
+ context.options.migrationAction ?? "push"
2881
+ );
2882
+ }
2883
+ }
2884
+ };
2885
+
2886
+ // src/apps/registry.ts
2887
+ var profiles = [opsProfile];
2888
+ function getAppProfile(appId) {
2889
+ return profiles.find((profile) => profile.id === appId);
2890
+ }
2891
+
2892
+ // src/apps/runAppCli.ts
2893
+ function assertAppCommand(options) {
2894
+ if (!options.action || !options.app) {
2895
+ throw new Error("Usage: polter app <setup|link|migrate|configure|install|update> <app>");
2896
+ }
2897
+ }
2898
+ async function runAppCli(options) {
2899
+ assertAppCommand(options);
2900
+ const profile = getAppProfile(options.app);
2901
+ if (!profile) {
2902
+ throw new Error(`Unknown app profile: ${options.app}`);
2903
+ }
2904
+ const projectRoot = profile.resolveProjectRoot(process.cwd(), options.path);
2905
+ process.stdout.write(
2906
+ `${pc3.bold("Polter")} ${pc3.dim("app workflow")} ${pc3.bold(profile.displayName)} ${pc3.dim(`(${options.action})`)}
2907
+
2908
+ `
2909
+ );
2910
+ return profile.run(options.action, {
2911
+ cwd: process.cwd(),
2912
+ projectRoot,
2913
+ options
2914
+ });
2915
+ }
2916
+
2917
+ // src/index.tsx
2918
+ async function main() {
2919
+ const parsed = parseCliArgs(process.argv.slice(2));
2920
+ if (parsed.mode === "help") {
2921
+ printCliHelp();
2922
+ return;
2923
+ }
2924
+ if (parsed.mode === "app") {
2925
+ const exitCode = await runAppCli(parsed.options);
2926
+ process.exit(exitCode);
2927
+ }
2928
+ render(React8.createElement(App));
2929
+ }
2930
+ main().catch((error) => {
2931
+ const message = error instanceof Error ? error.message : String(error);
2932
+ process.stderr.write(`${message}
2933
+ `);
2934
+ process.exit(1);
2935
+ });