@ollie-shop/cli 1.2.2 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ollie-shop/cli",
3
- "version": "1.2.2",
3
+ "version": "1.3.3",
4
4
  "description": "Ollie Shop CLI - Development tools for custom checkouts",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.tsx CHANGED
@@ -3,6 +3,9 @@ import { HelpCommand } from "./commands/help.js";
3
3
  import { LoginCommand } from "./commands/login.js";
4
4
  import { StartCommand } from "./commands/start.js";
5
5
 
6
+ // Inlined by tsup at build time from package.json (see tsup.config.ts).
7
+ declare const __OLLIE_CLI_VERSION__: string;
8
+
6
9
  interface AppProps {
7
10
  command: string;
8
11
  args: string[];
@@ -28,9 +31,13 @@ export function App({ command, args }: AppProps) {
28
31
  }
29
32
 
30
33
  function VersionCommand() {
34
+ const version =
35
+ typeof __OLLIE_CLI_VERSION__ === "string" && __OLLIE_CLI_VERSION__
36
+ ? __OLLIE_CLI_VERSION__
37
+ : "unknown";
31
38
  return (
32
39
  <Box>
33
- <Text>ollie v0.1.0</Text>
40
+ <Text>ollieshop v{version}</Text>
34
41
  </Box>
35
42
  );
36
43
  }
@@ -17,6 +17,43 @@ import {
17
17
 
18
18
  const ON_ERROR_VALUES = ["throw", "skip"] as const;
19
19
 
20
+ function parseTriggerFromFlags(
21
+ urlFlag: string | undefined,
22
+ expressionFlag: string | undefined,
23
+ ): { url: string; expression: string } | undefined {
24
+ if (urlFlag === undefined && expressionFlag === undefined) return undefined;
25
+ if (urlFlag === undefined || expressionFlag === undefined) {
26
+ throw new Error(
27
+ "Both --trigger-url and --trigger-expression are required when configuring a trigger.",
28
+ );
29
+ }
30
+ return {
31
+ url: validateTriggerUrl(urlFlag, "trigger-url"),
32
+ expression: validateRequired(expressionFlag, "trigger-expression"),
33
+ };
34
+ }
35
+
36
+ function parseTriggerFromData(
37
+ raw: unknown,
38
+ ): { url: string; expression: string } | undefined {
39
+ if (raw === undefined || raw === null) return undefined;
40
+ if (typeof raw !== "object" || Array.isArray(raw)) {
41
+ throw new Error(
42
+ 'Invalid trigger: must be an object with "url" and "expression" string properties.',
43
+ );
44
+ }
45
+ const obj = raw as Record<string, unknown>;
46
+ if (typeof obj.url !== "string" || typeof obj.expression !== "string") {
47
+ throw new Error(
48
+ 'Invalid trigger: both "url" and "expression" must be non-empty strings.',
49
+ );
50
+ }
51
+ return {
52
+ url: validateTriggerUrl(obj.url, "trigger.url"),
53
+ expression: validateRequired(obj.expression, "trigger.expression"),
54
+ };
55
+ }
56
+
20
57
  export async function functionCommand(parsed: ParsedArgs): Promise<void> {
21
58
  const sub = parsed.subcommand;
22
59
  if (sub === "create") return functionCreateCommand(parsed);
@@ -35,7 +72,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
35
72
  let input: {
36
73
  versionId: string;
37
74
  name: string;
38
- trigger?: string;
75
+ trigger?: { url: string; expression: string };
39
76
  active?: boolean;
40
77
  onError?: "throw" | "skip";
41
78
  priority?: number;
@@ -46,10 +83,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
46
83
  input = {
47
84
  versionId: validateUuid(raw.versionId, "versionId"),
48
85
  name: validateRequired(raw.name, "name"),
49
- trigger:
50
- raw.trigger !== undefined && raw.trigger !== null
51
- ? validateTriggerUrl(String(raw.trigger), "trigger")
52
- : undefined,
86
+ trigger: parseTriggerFromData(raw.trigger),
53
87
  active: raw.active === true,
54
88
  onError:
55
89
  raw.onError !== undefined && raw.onError !== null
@@ -61,7 +95,8 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
61
95
  : 0,
62
96
  };
63
97
  } else {
64
- const triggerFlag = getFlag(parsed.flags, "trigger");
98
+ const triggerUrlFlag = getFlag(parsed.flags, "trigger-url");
99
+ const triggerExpressionFlag = getFlag(parsed.flags, "trigger-expression");
65
100
  const onErrorFlag = getFlag(parsed.flags, "on-error");
66
101
  const priorityFlag = getFlag(parsed.flags, "priority");
67
102
  input = {
@@ -70,10 +105,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
70
105
  "version-id",
71
106
  ),
72
107
  name: validateRequired(getFlag(parsed.flags, "name", "n"), "name"),
73
- trigger:
74
- triggerFlag !== undefined
75
- ? validateTriggerUrl(triggerFlag, "trigger")
76
- : undefined,
108
+ trigger: parseTriggerFromFlags(triggerUrlFlag, triggerExpressionFlag),
77
109
  active: getBoolFlag(parsed.flags, "active"),
78
110
  onError:
79
111
  onErrorFlag !== undefined
@@ -41,7 +41,7 @@ export function HelpCommand() {
41
41
  <Box width={24}>
42
42
  <Text color="green">whoami</Text>
43
43
  </Box>
44
- <Text>Show current user and organization</Text>
44
+ <Text>Show current user (and linked store/org from ollie.json)</Text>
45
45
  </Box>
46
46
  <Box>
47
47
  <Box width={24}>
@@ -153,6 +153,10 @@ export function HelpCommand() {
153
153
  $ ollieshop store create --name "My Store" --platform vtex
154
154
  --platform-store-id mystore
155
155
  </Text>
156
+ <Text dimColor>
157
+ $ ollieshop store create --org UUID --name "My Store" --platform vtex
158
+ --platform-store-id mystore
159
+ </Text>
156
160
  <Text dimColor>
157
161
  $ ollieshop store create --data{" "}
158
162
  {
@@ -1,11 +1,10 @@
1
- import type { BuildContext, ServeOnRequestArgs } from "esbuild";
1
+ import type { ServeOnRequestArgs } from "esbuild";
2
2
  import { Box, Text, useApp, useInput } from "ink";
3
3
  import open from "open";
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
5
  import { loadConfig, resolveStage } from "../utils/config.js";
6
6
  import {
7
7
  type ComponentInfo,
8
- createBuildContext,
9
8
  discoverComponents,
10
9
  startDevServer,
11
10
  } from "../utils/esbuild.js";
@@ -55,7 +54,7 @@ export function StartCommand({ args }: StartCommandProps) {
55
54
  const [buildCount, setBuildCount] = useState(0);
56
55
  const [lastBuildTime, setLastBuildTime] = useState<Date | null>(null);
57
56
  const logIdRef = useRef(0);
58
- const ctxRef = useRef<BuildContext | null>(null);
57
+ const rebuildRef = useRef<(() => Promise<void>) | null>(null);
59
58
  const stopRef = useRef<(() => Promise<void>) | null>(null);
60
59
 
61
60
  // Parse args
@@ -121,28 +120,19 @@ export function StartCommand({ args }: StartCommandProps) {
121
120
  setComponents(found);
122
121
  setState({ status: "building" });
123
122
 
124
- // Create build context with discovered components
125
- const ctx = await createBuildContext(found, {
123
+ // Start dev server
124
+ const server = await startDevServer({
125
+ port: PORT,
126
126
  stage,
127
+ onRequest: handleRequest,
127
128
  onBuildEnd: (updatedComponents) => {
128
129
  setComponents(updatedComponents);
129
130
  setBuildCount((c) => c + 1);
130
131
  setLastBuildTime(new Date());
131
132
  },
132
133
  });
133
- ctxRef.current = ctx;
134
-
135
- // Do initial build (manifest is written by the plugin)
136
- await ctx.rebuild();
137
-
138
- if (!mounted) return;
139
-
140
- // Start dev server
141
- const server = await startDevServer(ctx, {
142
- port: PORT,
143
- onRequest: handleRequest,
144
- });
145
134
 
135
+ rebuildRef.current = server.rebuild;
146
136
  stopRef.current = server.stop;
147
137
 
148
138
  if (!mounted) {
@@ -190,7 +180,7 @@ export function StartCommand({ args }: StartCommandProps) {
190
180
 
191
181
  // Manual rebuild with 'r' (manifest is updated by the plugin)
192
182
  if (input === "r" && state.status === "running") {
193
- ctxRef.current?.rebuild();
183
+ rebuildRef.current?.();
194
184
  }
195
185
 
196
186
  // Open Studio in browser with 'o'
@@ -7,7 +7,11 @@ import {
7
7
  } from "../utils/output.js";
8
8
  import { type ParsedArgs, getFlag } from "../utils/parse-args.js";
9
9
  import { getAuthenticatedClient } from "../utils/supabase.js";
10
- import { validateEnum, validateRequired } from "../utils/validate.js";
10
+ import {
11
+ validateEnum,
12
+ validateRequired,
13
+ validateUuid,
14
+ } from "../utils/validate.js";
11
15
 
12
16
  export async function storeCommand(parsed: ParsedArgs): Promise<void> {
13
17
  const sub = parsed.subcommand;
@@ -63,13 +67,16 @@ async function storeCreateCommand(parsed: ParsedArgs): Promise<void> {
63
67
  };
64
68
  }
65
69
 
70
+ const orgFlag = getFlag(parsed.flags, "org");
71
+ const orgId = orgFlag ? validateUuid(orgFlag, "org") : undefined;
72
+
66
73
  if (parsed.global.dryRun) {
67
- outputDryRun("store.create", input, format);
74
+ outputDryRun("store.create", { ...input, orgId }, format);
68
75
  return;
69
76
  }
70
77
 
71
78
  const client = await getAuthenticatedClient();
72
- const result = await createStore(client, input);
79
+ const result = await createStore(client, input, orgId);
73
80
 
74
81
  outputResult(result, format, parsed.global.fields);
75
82
  if (result.error) process.exit(1);
@@ -88,8 +95,11 @@ async function storeListCommand(parsed: ParsedArgs): Promise<void> {
88
95
  const format = detectOutputFormat(parsed.global.output);
89
96
 
90
97
  try {
98
+ const orgFlag = getFlag(parsed.flags, "org");
99
+ const orgId = orgFlag ? validateUuid(orgFlag, "org") : undefined;
100
+
91
101
  const client = await getAuthenticatedClient();
92
- const result = await listStores(client);
102
+ const result = await listStores(client, orgId);
93
103
 
94
104
  outputResult(result, format, parsed.global.fields);
95
105
  if (result.error) process.exit(1);
@@ -1,6 +1,9 @@
1
+ import { getStoreById } from "../core/store.js";
1
2
  import { getCurrentUser } from "../utils/auth.js";
3
+ import { loadConfig, resolveStage } from "../utils/config.js";
2
4
  import { detectOutputFormat, outputResult } from "../utils/output.js";
3
5
  import type { ParsedArgs } from "../utils/parse-args.js";
6
+ import { getAuthenticatedClient } from "../utils/supabase.js";
4
7
 
5
8
  export async function whoamiCommand(parsed: ParsedArgs): Promise<void> {
6
9
  const format = detectOutputFormat(parsed.global.output);
@@ -9,20 +12,78 @@ export async function whoamiCommand(parsed: ParsedArgs): Promise<void> {
9
12
  if (!user) {
10
13
  outputResult(
11
14
  {
12
- error: { message: "Not logged in. Run `ollieshop login`." },
15
+ error: {
16
+ message: "Not logged in or session expired. Run `ollieshop login`.",
17
+ },
13
18
  },
14
19
  format,
15
20
  );
16
21
  process.exit(1);
17
22
  }
18
23
 
19
- outputResult(
20
- {
21
- data: {
22
- email: user.email,
24
+ try {
25
+ const stage = resolveStage(parsed.global.stage);
26
+ const config = await loadConfig({ stage });
27
+
28
+ // No project config: just report the authenticated user.
29
+ if (!config?.storeId) {
30
+ outputResult(
31
+ {
32
+ data: {
33
+ email: user.email,
34
+ hint: "No ollie.json found. Run `ollieshop init` to link a store.",
35
+ },
36
+ },
37
+ format,
38
+ parsed.global.fields,
39
+ );
40
+ return;
41
+ }
42
+
43
+ // Config present: resolve the store/org it points at, gated by access.
44
+ const client = await getAuthenticatedClient();
45
+ const result = await getStoreById(client, config.storeId);
46
+
47
+ if (result.error || !result.data) {
48
+ outputResult(
49
+ {
50
+ data: {
51
+ email: user.email,
52
+ storeId: config.storeId,
53
+ store: null,
54
+ note:
55
+ result.error?.message ??
56
+ "Store not found. Run `ollieshop login` or check the storeId in ollie.json.",
57
+ },
58
+ },
59
+ format,
60
+ parsed.global.fields,
61
+ );
62
+ return;
63
+ }
64
+
65
+ const store = result.data;
66
+ outputResult(
67
+ {
68
+ data: {
69
+ email: user.email,
70
+ orgId: store.organizationId,
71
+ org: store.organizationName,
72
+ storeId: store.id,
73
+ store: store.name,
74
+ ...(config.versionId ? { versionId: config.versionId } : {}),
75
+ },
23
76
  },
24
- },
25
- format,
26
- parsed.global.fields,
27
- );
77
+ format,
78
+ parsed.global.fields,
79
+ );
80
+ } catch (err) {
81
+ outputResult(
82
+ {
83
+ error: { message: err instanceof Error ? err.message : String(err) },
84
+ },
85
+ format,
86
+ );
87
+ process.exit(1);
88
+ }
28
89
  }
@@ -4,7 +4,7 @@ import { functionCreateSchema } from "./schema.js";
4
4
  export interface CreateFunctionInput {
5
5
  versionId: string;
6
6
  name: string;
7
- trigger?: string;
7
+ trigger?: { url: string; expression: string };
8
8
  active?: boolean;
9
9
  onError?: "throw" | "skip";
10
10
  priority?: number;
@@ -65,11 +65,16 @@ export const functionCreateSchema = z.object({
65
65
  versionId: z.string().uuid().describe("Parent version UUID"),
66
66
  name: z.string().min(1).describe("Function name"),
67
67
  trigger: z
68
- .string()
69
- .min(1)
68
+ .object({
69
+ url: z.string().min(1).describe("Trigger URL — absolute http(s) URL"),
70
+ expression: z
71
+ .string()
72
+ .min(1)
73
+ .describe("JSONata-like match expression (e.g. 'method in [\"GET\"]')"),
74
+ })
70
75
  .optional()
71
76
  .describe(
72
- "Function trigger URL absolute http(s) URL or relative path starting with /",
77
+ "Function trigger — both url and expression are required together",
73
78
  ),
74
79
  active: z
75
80
  .boolean()
package/src/core/store.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
- import { getOrganizationId } from "../utils/supabase.js";
2
+ import { getOrganizationId, unwrapToOne } from "../utils/supabase.js";
3
3
  import { storeCreateSchema } from "./schema.js";
4
4
 
5
5
  export interface CreateStoreInput {
@@ -24,6 +24,7 @@ export interface StoreRecord {
24
24
  export async function createStore(
25
25
  client: SupabaseClient,
26
26
  input: CreateStoreInput,
27
+ orgId?: string,
27
28
  ): Promise<{ data?: { id: string }; error?: { message: string } }> {
28
29
  // Validate input against schema
29
30
  const parsed = storeCreateSchema.safeParse(input);
@@ -33,7 +34,7 @@ export async function createStore(
33
34
  };
34
35
  }
35
36
 
36
- const organizationId = await getOrganizationId(client);
37
+ const organizationId = await getOrganizationId(client, orgId);
37
38
 
38
39
  const { data, error } = await client
39
40
  .from("stores")
@@ -55,10 +56,56 @@ export async function createStore(
55
56
  return { data: { id: data.id } };
56
57
  }
57
58
 
59
+ export interface StoreWithOrg {
60
+ id: string;
61
+ name: string;
62
+ organizationId: string;
63
+ organizationName: string | null;
64
+ }
65
+
66
+ // Resolves a store + org by id; RLS returns no row when the user lacks access.
67
+ export async function getStoreById(
68
+ client: SupabaseClient,
69
+ storeId: string,
70
+ ): Promise<{ data?: StoreWithOrg; error?: { message: string } }> {
71
+ const { data, error } = await client
72
+ .from("stores")
73
+ .select("id, name, organization_id, organizations(id, name)")
74
+ .eq("id", storeId)
75
+ .maybeSingle();
76
+
77
+ if (error) {
78
+ return { error: { message: error.message } };
79
+ }
80
+
81
+ if (!data) {
82
+ return {
83
+ error: {
84
+ message: `Store ${storeId} not found or you don't have access to it.`,
85
+ },
86
+ };
87
+ }
88
+
89
+ const org = unwrapToOne<{ name?: string }>(
90
+ (data as { organizations?: unknown }).organizations,
91
+ );
92
+ const orgName = org?.name ?? null;
93
+
94
+ return {
95
+ data: {
96
+ id: data.id as string,
97
+ name: data.name as string,
98
+ organizationId: data.organization_id as string,
99
+ organizationName: orgName,
100
+ },
101
+ };
102
+ }
103
+
58
104
  export async function listStores(
59
105
  client: SupabaseClient,
106
+ orgId?: string,
60
107
  ): Promise<{ data?: StoreRecord[]; error?: { message: string } }> {
61
- const organizationId = await getOrganizationId(client);
108
+ const organizationId = await getOrganizationId(client, orgId);
62
109
 
63
110
  const { data, error } = await client
64
111
  .from("stores")
package/src/utils/auth.ts CHANGED
@@ -231,6 +231,10 @@ export async function getCurrentUser(): Promise<{ email: string } | null> {
231
231
 
232
232
  try {
233
233
  const decoded = jwtDecode<JwtPayload>(credentials.accessToken);
234
+ // Reject expired sessions (local `exp` check; misses server-side revocation).
235
+ if (decoded.exp && decoded.exp * 1000 <= Date.now()) {
236
+ return null;
237
+ }
234
238
  return decoded.email ? { email: decoded.email } : null;
235
239
  } catch {
236
240
  return null;
@@ -1,3 +1,4 @@
1
+ import { watch } from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import http from "node:http";
3
4
  import path from "node:path";
@@ -221,32 +222,95 @@ export interface ServeResult {
221
222
  /**
222
223
  * Starts the esbuild serve + watch mode with a proxy server.
223
224
  * The proxy handles /bundle/:componentName requests and forwards others to esbuild.
224
- * Returns the server info and a way to stop it.
225
+ * Owns the esbuild context lifecycle and recreates it when a component folder is
226
+ * added or removed. Returns the server info, a manual rebuild, and a way to stop it.
225
227
  */
226
228
  export async function startDevServer(
227
- ctx: esbuild.BuildContext,
228
229
  options: {
229
230
  port?: number;
230
231
  host?: string;
231
232
  cwd?: string;
233
+ stage?: string;
232
234
  onRequest?: (args: esbuild.ServeOnRequestArgs) => void;
233
- onRebuild?: (result: esbuild.BuildResult) => void;
235
+ onBuildEnd?: (components: ComponentInfo[], result: BuildResult) => void;
234
236
  } = {},
235
- ): Promise<ServeResult & { stop: () => Promise<void> }> {
236
- const { port = 4000, host = "localhost", cwd = process.cwd() } = options;
237
+ ): Promise<
238
+ ServeResult & { rebuild: () => Promise<void>; stop: () => Promise<void> }
239
+ > {
240
+ const {
241
+ port = 4000,
242
+ host = "localhost",
243
+ cwd = process.cwd(),
244
+ stage,
245
+ onRequest,
246
+ onBuildEnd,
247
+ } = options;
237
248
 
238
249
  const servedir = path.join(cwd, "node_modules/.ollie", "build");
250
+ const componentsDir = path.join(cwd, "components");
251
+ const internalPort = port + 1;
239
252
 
240
- // Start watching for changes
241
- await ctx.watch();
253
+ let ctx: esbuild.BuildContext | null = null;
254
+ let entryNames = new Set<string>();
242
255
 
243
- // Start esbuild server on internal port (proxy will forward to it)
244
- const internalPort = port + 1;
245
- await ctx.serve({
246
- port: internalPort,
247
- host,
248
- servedir,
249
- onRequest: options.onRequest,
256
+ async function buildAndServe(components: ComponentInfo[]): Promise<void> {
257
+ entryNames = new Set(components.map((c) => c.name));
258
+ ctx = await createBuildContext(components, { cwd, stage, onBuildEnd });
259
+ await ctx.rebuild();
260
+ await ctx.watch();
261
+ await ctx.serve({ port: internalPort, host, servedir, onRequest });
262
+ }
263
+
264
+ async function recreate(): Promise<void> {
265
+ const components = await discoverComponents({ cwd, stage });
266
+ const oldCtx = ctx;
267
+ ctx = null;
268
+ if (oldCtx) await oldCtx.dispose();
269
+ await buildAndServe(components);
270
+ }
271
+
272
+ await buildAndServe(await discoverComponents({ cwd, stage }));
273
+
274
+ async function currentComponentNames(): Promise<Set<string>> {
275
+ try {
276
+ const entries = await glob("*/index.tsx", { cwd: componentsDir });
277
+ return new Set(entries.map((e) => path.dirname(e)));
278
+ } catch {
279
+ return new Set();
280
+ }
281
+ }
282
+
283
+ // Recreate the context when the set of component folders changes
284
+ let watchTimer: ReturnType<typeof setTimeout> | null = null;
285
+ let recreating = false;
286
+ let pending = false;
287
+
288
+ async function maybeRecreate(): Promise<void> {
289
+ if (recreating) {
290
+ pending = true;
291
+ return;
292
+ }
293
+ recreating = true;
294
+ try {
295
+ do {
296
+ pending = false;
297
+ const names = await currentComponentNames();
298
+ const changed =
299
+ names.size !== entryNames.size ||
300
+ [...names].some((n) => !entryNames.has(n));
301
+ if (!changed) break;
302
+ await recreate();
303
+ } while (pending);
304
+ } catch (err) {
305
+ console.error("[DevServer] Failed to reload components:", err);
306
+ } finally {
307
+ recreating = false;
308
+ }
309
+ }
310
+
311
+ const componentsWatcher = watch(componentsDir, { recursive: true }, () => {
312
+ if (watchTimer) clearTimeout(watchTimer);
313
+ watchTimer = setTimeout(maybeRecreate, 150);
250
314
  });
251
315
 
252
316
  // Create proxy server that handles /bundle/* and forwards to esbuild
@@ -356,6 +420,21 @@ export async function startDevServer(
356
420
  // File doesn't exist, start fresh
357
421
  }
358
422
 
423
+ if (
424
+ typeof updates.id === "string" &&
425
+ typeof existingMeta.id === "string" &&
426
+ existingMeta.id !== updates.id
427
+ ) {
428
+ res.statusCode = 409;
429
+ res.setHeader("Content-Type", "application/json");
430
+ res.end(
431
+ JSON.stringify({
432
+ error: `id mismatch: ${componentName} is linked to ${existingMeta.id}`,
433
+ }),
434
+ );
435
+ return;
436
+ }
437
+
359
438
  // Merge updates
360
439
  const newMeta = { ...existingMeta, ...updates };
361
440
 
@@ -435,9 +514,14 @@ export async function startDevServer(
435
514
  return {
436
515
  host,
437
516
  port,
517
+ rebuild: async () => {
518
+ await ctx?.rebuild();
519
+ },
438
520
  stop: async () => {
521
+ if (watchTimer) clearTimeout(watchTimer);
522
+ componentsWatcher.close();
439
523
  proxyServer.close();
440
- await ctx.dispose();
524
+ await ctx?.dispose();
441
525
  },
442
526
  };
443
527
  }
@@ -3,6 +3,7 @@ export interface GlobalFlags {
3
3
  dryRun: boolean;
4
4
  fields?: string[];
5
5
  data?: string;
6
+ stage?: string;
6
7
  }
7
8
 
8
9
  export interface ParsedArgs {
@@ -63,6 +64,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
63
64
  .map((f) => f.trim())
64
65
  : undefined,
65
66
  data: (flags.data as string) || (flags.d as string) || undefined,
67
+ stage: (flags.stage as string) || (flags.s as string) || undefined,
66
68
  };
67
69
 
68
70
  return { command, subcommand, flags, global, positional };