@sentroy-co/client-sdk 2.13.9 → 2.15.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.
- package/README.md +53 -5
- package/dist/auth/client.d.ts.map +1 -1
- package/dist/auth/client.js +8 -1
- package/dist/auth/client.js.map +1 -1
- package/dist/auth/react/index.js +1 -1
- package/dist/auth/react/index.js.map +1 -1
- package/dist/auth/react-native/index.d.ts +93 -0
- package/dist/auth/react-native/index.d.ts.map +1 -0
- package/dist/auth/react-native/index.js +106 -0
- package/dist/auth/react-native/index.js.map +1 -0
- package/dist/cli/ai.d.ts +35 -0
- package/dist/cli/ai.d.ts.map +1 -0
- package/dist/cli/ai.js +399 -0
- package/dist/cli/ai.js.map +1 -0
- package/dist/cli/args.d.ts +62 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +199 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/env.d.ts.map +1 -1
- package/dist/cli/env.js +8 -2
- package/dist/cli/env.js.map +1 -1
- package/dist/cli/format.d.ts +37 -0
- package/dist/cli/format.d.ts.map +1 -0
- package/dist/cli/format.js +129 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +128 -25
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mail.d.ts +25 -0
- package/dist/cli/mail.d.ts.map +1 -0
- package/dist/cli/mail.js +253 -0
- package/dist/cli/mail.js.map +1 -0
- package/dist/cli/storage.d.ts +28 -0
- package/dist/cli/storage.d.ts.map +1 -0
- package/dist/cli/storage.js +189 -0
- package/dist/cli/storage.js.map +1 -0
- package/package.json +9 -2
- package/skill/SKILL.md +577 -0
- package/src/auth/client.ts +8 -1
- package/src/auth/react/index.tsx +1 -1
- package/src/auth/react-native/index.ts +157 -0
- package/src/cli/ai.ts +440 -0
- package/src/cli/args.ts +225 -0
- package/src/cli/env.ts +10 -2
- package/src/cli/format.ts +147 -0
- package/src/cli/index.ts +147 -25
- package/src/cli/mail.ts +363 -0
- package/src/cli/storage.ts +307 -0
package/src/cli/args.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI utilities — flag parser, ANSI helpers, auth resolver, HTTP
|
|
3
|
+
* client, fail/info/ok/warn loggers. Reused by every subcommand
|
|
4
|
+
* (env, mail, storage, ai).
|
|
5
|
+
*
|
|
6
|
+
* `env.ts` historically inlined these; new subcommands import them from
|
|
7
|
+
* here to stay DRY and to allow extending `VALUE_FLAGS` in one place.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ParsedArgs {
|
|
11
|
+
positional: string[]
|
|
12
|
+
flags: Record<string, string | boolean>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SharedOpts {
|
|
16
|
+
token: string
|
|
17
|
+
baseUrl: string
|
|
18
|
+
/** Company slug — mail/storage commands need it; env doesn't (token is
|
|
19
|
+
* vault-scoped). Resolved from --company-slug, $SENTROY_COMPANY_SLUG,
|
|
20
|
+
* or thrown error. */
|
|
21
|
+
companySlug?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── ANSI helpers (no deps) ──────────────────────────────────────────────
|
|
25
|
+
// Honor https://no-color.org and FORCE_COLOR override before falling back
|
|
26
|
+
// to TTY detection. Captured at module load — wrappers/tests that need to
|
|
27
|
+
// override should set the env var before requiring this module.
|
|
28
|
+
const supportsColor = (() => {
|
|
29
|
+
if (typeof process === "undefined") return false
|
|
30
|
+
if (process.env.NO_COLOR) return false
|
|
31
|
+
if (process.env.FORCE_COLOR) return process.env.FORCE_COLOR !== "0"
|
|
32
|
+
return Boolean(process.stdout?.isTTY) && process.env.TERM !== "dumb"
|
|
33
|
+
})()
|
|
34
|
+
|
|
35
|
+
export const c = {
|
|
36
|
+
bold: (s: string) => (supportsColor ? `\x1b[1m${s}\x1b[0m` : s),
|
|
37
|
+
dim: (s: string) => (supportsColor ? `\x1b[2m${s}\x1b[0m` : s),
|
|
38
|
+
red: (s: string) => (supportsColor ? `\x1b[31m${s}\x1b[0m` : s),
|
|
39
|
+
green: (s: string) => (supportsColor ? `\x1b[32m${s}\x1b[0m` : s),
|
|
40
|
+
yellow: (s: string) => (supportsColor ? `\x1b[33m${s}\x1b[0m` : s),
|
|
41
|
+
cyan: (s: string) => (supportsColor ? `\x1b[36m${s}\x1b[0m` : s),
|
|
42
|
+
magenta: (s: string) => (supportsColor ? `\x1b[35m${s}\x1b[0m` : s),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function fail(msg: string): never {
|
|
46
|
+
process.stderr.write(`${c.red("✗")} ${msg}\n`)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function info(msg: string): void {
|
|
51
|
+
process.stdout.write(`${c.cyan("→")} ${msg}\n`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ok(msg: string): void {
|
|
55
|
+
process.stdout.write(`${c.green("✓")} ${msg}\n`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function warn(msg: string): void {
|
|
59
|
+
process.stdout.write(`${c.yellow("⚠")} ${msg}\n`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Flags that take a value as a separate token (`--flag value`). All other
|
|
64
|
+
* `--flag` tokens are booleans. The `--flag=value` form always works
|
|
65
|
+
* regardless of this list.
|
|
66
|
+
*/
|
|
67
|
+
const VALUE_FLAGS = new Set([
|
|
68
|
+
"token",
|
|
69
|
+
"url",
|
|
70
|
+
"company-slug",
|
|
71
|
+
"output",
|
|
72
|
+
"page",
|
|
73
|
+
"limit",
|
|
74
|
+
"skip",
|
|
75
|
+
"mailbox",
|
|
76
|
+
"folder",
|
|
77
|
+
"q",
|
|
78
|
+
"domain",
|
|
79
|
+
"reason",
|
|
80
|
+
"status",
|
|
81
|
+
"from",
|
|
82
|
+
"to",
|
|
83
|
+
"days",
|
|
84
|
+
"type",
|
|
85
|
+
"sort",
|
|
86
|
+
"dir",
|
|
87
|
+
"out",
|
|
88
|
+
"source",
|
|
89
|
+
"pin",
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
export function parseFlags(args: string[]): ParsedArgs {
|
|
93
|
+
const positional: string[] = []
|
|
94
|
+
const flags: Record<string, string | boolean> = {}
|
|
95
|
+
for (let i = 0; i < args.length; i++) {
|
|
96
|
+
const a = args[i]!
|
|
97
|
+
if (!a.startsWith("--")) {
|
|
98
|
+
positional.push(a)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
const body = a.slice(2)
|
|
102
|
+
const eq = body.indexOf("=")
|
|
103
|
+
if (eq >= 0) {
|
|
104
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
if (VALUE_FLAGS.has(body)) {
|
|
108
|
+
const next = args[i + 1]
|
|
109
|
+
if (next === undefined || next.startsWith("--")) {
|
|
110
|
+
fail(
|
|
111
|
+
`flag --${body} requires a value (use --${body}=value or --${body} value)`,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
flags[body] = next
|
|
115
|
+
i++
|
|
116
|
+
} else {
|
|
117
|
+
flags[body] = true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { positional, flags }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve shared opts for general (non-vault) Sentroy CLI commands.
|
|
125
|
+
*
|
|
126
|
+
* Token resolution priority:
|
|
127
|
+
* 1. --token=stk_... flag
|
|
128
|
+
* 2. SENTROY_API_KEY env var
|
|
129
|
+
* 3. SENTROY_ENV_API_KEY env var (back-compat — same token namespace works)
|
|
130
|
+
*
|
|
131
|
+
* Base URL priority:
|
|
132
|
+
* 1. --url=https://... flag
|
|
133
|
+
* 2. SENTROY_API_URL env var
|
|
134
|
+
* 3. SENTROY_ENV_API_URL env var
|
|
135
|
+
* 4. https://sentroy.com
|
|
136
|
+
*
|
|
137
|
+
* Company slug priority (REQUIRED for mail/storage):
|
|
138
|
+
* 1. --company-slug=<slug> flag
|
|
139
|
+
* 2. SENTROY_COMPANY_SLUG env var
|
|
140
|
+
*/
|
|
141
|
+
export function resolveSharedOpts(
|
|
142
|
+
flags: Record<string, string | boolean>,
|
|
143
|
+
opts: { requireCompanySlug?: boolean } = {},
|
|
144
|
+
): SharedOpts {
|
|
145
|
+
const token =
|
|
146
|
+
(typeof flags.token === "string" ? flags.token : null) ??
|
|
147
|
+
process.env.SENTROY_API_KEY ??
|
|
148
|
+
process.env.SENTROY_ENV_API_KEY ??
|
|
149
|
+
null
|
|
150
|
+
if (!token) {
|
|
151
|
+
fail(
|
|
152
|
+
"no token. Pass --token=stk_... or set SENTROY_API_KEY (or SENTROY_ENV_API_KEY for vault) in your environment.",
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
const baseUrl = (
|
|
156
|
+
(typeof flags.url === "string" ? flags.url : null) ??
|
|
157
|
+
process.env.SENTROY_API_URL ??
|
|
158
|
+
process.env.SENTROY_ENV_API_URL ??
|
|
159
|
+
"https://sentroy.com"
|
|
160
|
+
).replace(/\/+$/, "")
|
|
161
|
+
|
|
162
|
+
const companySlug =
|
|
163
|
+
(typeof flags["company-slug"] === "string"
|
|
164
|
+
? (flags["company-slug"] as string)
|
|
165
|
+
: null) ??
|
|
166
|
+
process.env.SENTROY_COMPANY_SLUG ??
|
|
167
|
+
undefined
|
|
168
|
+
if (opts.requireCompanySlug && !companySlug) {
|
|
169
|
+
fail(
|
|
170
|
+
"no company slug. Pass --company-slug=<slug> or set SENTROY_COMPANY_SLUG in your environment.",
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
return { token, baseUrl, companySlug }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Thin fetch wrapper used by all subcommands that hit the Sentroy REST
|
|
178
|
+
* API. Strips trailing slashes from baseUrl, adds Bearer auth, unwraps
|
|
179
|
+
* `{success, data}` envelope, raises on non-2xx with a useful message.
|
|
180
|
+
*/
|
|
181
|
+
export async function apiFetch<T = unknown>(
|
|
182
|
+
shared: SharedOpts,
|
|
183
|
+
path: string,
|
|
184
|
+
init?: RequestInit,
|
|
185
|
+
): Promise<T> {
|
|
186
|
+
const res = await fetch(`${shared.baseUrl}${path}`, {
|
|
187
|
+
...init,
|
|
188
|
+
headers: {
|
|
189
|
+
Authorization: `Bearer ${shared.token}`,
|
|
190
|
+
...(init?.body ? { "Content-Type": "application/json" } : {}),
|
|
191
|
+
...(init?.headers ?? {}),
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
const text = await res.text()
|
|
195
|
+
let body: unknown
|
|
196
|
+
try {
|
|
197
|
+
body = text ? JSON.parse(text) : null
|
|
198
|
+
} catch {
|
|
199
|
+
body = text
|
|
200
|
+
}
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
// Sentroy returns different envelopes depending on handler; try the
|
|
203
|
+
// common shapes in priority order before falling back to HTTP status.
|
|
204
|
+
let errMsg = `HTTP ${res.status} ${res.statusText}`.trim()
|
|
205
|
+
if (body && typeof body === "object") {
|
|
206
|
+
const b = body as Record<string, unknown>
|
|
207
|
+
if (typeof b.error === "string") errMsg = b.error
|
|
208
|
+
else if (typeof b.message === "string") errMsg = b.message
|
|
209
|
+
else if (
|
|
210
|
+
b.error &&
|
|
211
|
+
typeof b.error === "object" &&
|
|
212
|
+
typeof (b.error as Record<string, unknown>).message === "string"
|
|
213
|
+
) {
|
|
214
|
+
errMsg = (b.error as Record<string, string>).message
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`${path}: ${errMsg}`)
|
|
218
|
+
}
|
|
219
|
+
// Envelope: {success: true, data: T} (most endpoints) or raw {data: T}.
|
|
220
|
+
// Some return body directly without envelope; fall back to body.
|
|
221
|
+
if (body && typeof body === "object" && "data" in body) {
|
|
222
|
+
return (body as { data: T }).data
|
|
223
|
+
}
|
|
224
|
+
return body as T
|
|
225
|
+
}
|
package/src/cli/env.ts
CHANGED
|
@@ -283,6 +283,7 @@ export async function cmdPull(args: string[]): Promise<void> {
|
|
|
283
283
|
const { positional, flags } = parseFlags(args)
|
|
284
284
|
const file = positional[0] ?? DEFAULT_FILE
|
|
285
285
|
const force = !!flags.force
|
|
286
|
+
const publicOnly = !!flags["public-only"]
|
|
286
287
|
const shared = resolveSharedOpts(flags)
|
|
287
288
|
|
|
288
289
|
const target = path.resolve(process.cwd(), file)
|
|
@@ -291,7 +292,12 @@ export async function cmdPull(args: string[]): Promise<void> {
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
info(`fetching from ${shared.baseUrl}…`)
|
|
294
|
-
|
|
295
|
+
// Browser-safe subset (public-only) hits a dedicated endpoint that
|
|
296
|
+
// strips secrets — useful for `.env.public` files committed to repos.
|
|
297
|
+
const endpoint = publicOnly
|
|
298
|
+
? "/api/env-vault/public"
|
|
299
|
+
: "/api/env-vault/fetch"
|
|
300
|
+
const remote = await http<FetchResponse>(shared, endpoint)
|
|
295
301
|
const entries: DotenvEntry[] = remote.variables.map((v) => ({
|
|
296
302
|
key: v.key,
|
|
297
303
|
value: v.value,
|
|
@@ -301,7 +307,9 @@ export async function cmdPull(args: string[]): Promise<void> {
|
|
|
301
307
|
const text = serializeDotenv(entries)
|
|
302
308
|
fs.writeFileSync(target, text, "utf8")
|
|
303
309
|
ok(
|
|
304
|
-
`wrote ${entries.length} variable(s) to ${file} (${remote.project}/${remote.environment})
|
|
310
|
+
`wrote ${entries.length} variable(s) to ${file} (${remote.project}/${remote.environment})${
|
|
311
|
+
publicOnly ? " [public-only]" : ""
|
|
312
|
+
}`,
|
|
305
313
|
)
|
|
306
314
|
}
|
|
307
315
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatters — table (human-friendly) + json (script/automation).
|
|
3
|
+
*
|
|
4
|
+
* CLI commands accept `--output=json|table` (default: table) and call
|
|
5
|
+
* `printRows(rows, columns, flags)`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { c } from "./args"
|
|
9
|
+
|
|
10
|
+
export interface Column<T> {
|
|
11
|
+
/** Header text shown in table mode (uppercase recommended). */
|
|
12
|
+
header: string
|
|
13
|
+
/** Value extractor; should return a primitive that .toString()'s cleanly. */
|
|
14
|
+
get: (row: T) => string | number | null | undefined
|
|
15
|
+
/** Max display width — longer values get truncated with `…`. Default 40. */
|
|
16
|
+
maxWidth?: number
|
|
17
|
+
/** Right-align numeric columns. Default false. */
|
|
18
|
+
alignRight?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Print rows as a formatted table (or JSON if --output=json).
|
|
23
|
+
*
|
|
24
|
+
* Table mode uses 2-space gap between columns, dim header line, no
|
|
25
|
+
* vertical separators (keeps copy-paste clean). Empty cells render as
|
|
26
|
+
* `c.dim("—")`.
|
|
27
|
+
*/
|
|
28
|
+
export function printRows<T>(
|
|
29
|
+
rows: T[],
|
|
30
|
+
columns: Column<T>[],
|
|
31
|
+
flags: Record<string, string | boolean>,
|
|
32
|
+
): void {
|
|
33
|
+
const output =
|
|
34
|
+
typeof flags.output === "string" ? flags.output.toLowerCase() : "table"
|
|
35
|
+
if (output === "json") {
|
|
36
|
+
// For JSON we dump the raw row objects, not the column projection —
|
|
37
|
+
// scripts may want fields beyond what we show in the table.
|
|
38
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + "\n")
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (rows.length === 0) {
|
|
42
|
+
process.stdout.write(c.dim("(no results)") + "\n")
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
// Compute string cells + widths
|
|
46
|
+
const cells: string[][] = rows.map((row) =>
|
|
47
|
+
columns.map((col) => {
|
|
48
|
+
const v = col.get(row)
|
|
49
|
+
if (v === null || v === undefined || v === "") return ""
|
|
50
|
+
const s = String(v)
|
|
51
|
+
const max = col.maxWidth ?? 40
|
|
52
|
+
if (s.length > max) return s.slice(0, max - 1) + "…"
|
|
53
|
+
return s
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
const widths = columns.map((col, i) => {
|
|
57
|
+
const headerWidth = col.header.length
|
|
58
|
+
const rowMax = cells.reduce((m, r) => Math.max(m, (r[i] ?? "").length), 0)
|
|
59
|
+
return Math.max(headerWidth, rowMax)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Header
|
|
63
|
+
const headerLine = columns
|
|
64
|
+
.map((col, i) => padCell(col.header, widths[i]!, col.alignRight ?? false))
|
|
65
|
+
.join(" ")
|
|
66
|
+
process.stdout.write(c.dim(headerLine) + "\n")
|
|
67
|
+
|
|
68
|
+
// Underline
|
|
69
|
+
const underline = widths.map((w) => "─".repeat(w)).join(" ")
|
|
70
|
+
process.stdout.write(c.dim(underline) + "\n")
|
|
71
|
+
|
|
72
|
+
// Rows
|
|
73
|
+
for (const cellRow of cells) {
|
|
74
|
+
const line = columns
|
|
75
|
+
.map((col, i) => {
|
|
76
|
+
const v = cellRow[i] ?? ""
|
|
77
|
+
const padded = padCell(v || c.dim("—"), widths[i]!, col.alignRight ?? false)
|
|
78
|
+
return padded
|
|
79
|
+
})
|
|
80
|
+
.join(" ")
|
|
81
|
+
process.stdout.write(line + "\n")
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write("\n" + c.dim(`${rows.length} row${rows.length === 1 ? "" : "s"}`) + "\n")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function padCell(text: string, width: number, alignRight: boolean): string {
|
|
87
|
+
// ANSI escape sequences mess up String.prototype.padEnd length math;
|
|
88
|
+
// strip color codes for width calculation only.
|
|
89
|
+
const visible = text.replace(/\x1b\[[0-9;]*m/g, "")
|
|
90
|
+
const pad = " ".repeat(Math.max(0, width - visible.length))
|
|
91
|
+
return alignRight ? pad + text : text + pad
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Print a single object as a key/value detail panel.
|
|
96
|
+
* Used by `<group> <resource> get <id>` commands.
|
|
97
|
+
*/
|
|
98
|
+
export function printDetail<T extends Record<string, unknown>>(
|
|
99
|
+
obj: T,
|
|
100
|
+
flags: Record<string, string | boolean>,
|
|
101
|
+
): void {
|
|
102
|
+
const output =
|
|
103
|
+
typeof flags.output === "string" ? flags.output.toLowerCase() : "table"
|
|
104
|
+
if (output === "json") {
|
|
105
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n")
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
const keys = Object.keys(obj)
|
|
109
|
+
if (keys.length === 0) {
|
|
110
|
+
process.stdout.write(c.dim("(empty)") + "\n")
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
const longest = keys.reduce((m, k) => Math.max(m, k.length), 0)
|
|
114
|
+
for (const k of keys) {
|
|
115
|
+
const v = obj[k]
|
|
116
|
+
const valStr =
|
|
117
|
+
v === null || v === undefined
|
|
118
|
+
? c.dim("—")
|
|
119
|
+
: typeof v === "object"
|
|
120
|
+
? JSON.stringify(v)
|
|
121
|
+
: String(v)
|
|
122
|
+
process.stdout.write(`${c.dim(k.padEnd(longest))} ${valStr}\n`)
|
|
123
|
+
}
|
|
124
|
+
process.stdout.write("\n")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Localized string helper — Sentroy mail/templates fields can be either a
|
|
129
|
+
* plain string or `Record<locale, string>` (e.g. `{tr, en}`). For table
|
|
130
|
+
* display, pick "en" fallback "tr" fallback first available, otherwise
|
|
131
|
+
* the raw string.
|
|
132
|
+
*/
|
|
133
|
+
export function loc(value: unknown, preferred = "en"): string {
|
|
134
|
+
if (value === null || value === undefined) return ""
|
|
135
|
+
if (typeof value === "string") return value
|
|
136
|
+
if (typeof value === "object") {
|
|
137
|
+
const obj = value as Record<string, unknown>
|
|
138
|
+
const direct = obj[preferred]
|
|
139
|
+
if (typeof direct === "string") return direct
|
|
140
|
+
const tr = obj.tr
|
|
141
|
+
if (typeof tr === "string") return tr
|
|
142
|
+
for (const k of Object.keys(obj)) {
|
|
143
|
+
if (typeof obj[k] === "string") return obj[k] as string
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return ""
|
|
147
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `sentroy` — CLI entry point.
|
|
3
3
|
*
|
|
4
|
-
* Subcommand router with no third-party deps.
|
|
5
|
-
*
|
|
4
|
+
* Subcommand router with no third-party deps. Groups:
|
|
5
|
+
* - env Vault sync (push/pull/list/diff) — vault-scoped token
|
|
6
|
+
* - mail Mail resource queries (templates/domains/mailboxes/...)
|
|
7
|
+
* - storage Storage resource queries (buckets/media/usage)
|
|
8
|
+
* - ai AI Skill install (Claude/Cursor/Windsurf/AGENTS.md)
|
|
9
|
+
*
|
|
10
|
+
* Shared helpers live in `./args` (parseFlags, resolveSharedOpts, apiFetch,
|
|
11
|
+
* c, fail, info, ok, warn) and `./format` (printRows, printDetail, loc).
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import { cmdPush, cmdPull, cmdList, cmdDiff } from "./env"
|
|
15
|
+
import { MAIL_HANDLERS } from "./mail"
|
|
16
|
+
import { STORAGE_HANDLERS } from "./storage"
|
|
17
|
+
import { AI_HANDLERS } from "./ai"
|
|
9
18
|
|
|
10
19
|
const VERSION = "__VERSION__" // replaced at runtime via package.json read
|
|
11
20
|
|
|
@@ -33,9 +42,41 @@ const ENV_SUBCOMMANDS: Record<string, SubCommand> = {
|
|
|
33
42
|
},
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
// MAIL/STORAGE/AI subcommands use sub-sub-command pattern:
|
|
46
|
+
// `sentroy mail templates list` → MAIL_HANDLERS["templates.list"]
|
|
47
|
+
// `sentroy mail logs get <id>` → MAIL_HANDLERS["logs.get"]
|
|
48
|
+
//
|
|
49
|
+
// Router joins argv[1] + argv[2] with "." for resource+verb lookup.
|
|
50
|
+
// `sentroy mail analytics` has no verb → MAIL_HANDLERS["analytics"].
|
|
51
|
+
// `sentroy ai install` has no resource → AI_HANDLERS["install"].
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Maps `mail templates list` → `templatesList`, `storage buckets get` →
|
|
55
|
+
* `bucketsGet`. If verb missing, tries plain resource key (e.g. `analytics`
|
|
56
|
+
* or `usage`). Returns the handler fn + how many argv tokens it consumed
|
|
57
|
+
* past the group (2 = resource+verb, 1 = resource only) so caller can
|
|
58
|
+
* slice the rest as args.
|
|
59
|
+
*/
|
|
60
|
+
function resolveHandler(
|
|
61
|
+
handlers: Record<string, (args: string[]) => Promise<void> | void>,
|
|
62
|
+
resource: string,
|
|
63
|
+
verb: string | undefined,
|
|
64
|
+
): { fn: (args: string[]) => Promise<void> | void; consumed: number } | null {
|
|
65
|
+
// Flag tokens are NOT verbs (e.g. `mail analytics --days=7`).
|
|
66
|
+
const verbToken = verb && !verb.startsWith("--") ? verb : undefined
|
|
67
|
+
if (verbToken) {
|
|
68
|
+
const camel =
|
|
69
|
+
resource + verbToken.charAt(0).toUpperCase() + verbToken.slice(1)
|
|
70
|
+
const h = handlers[camel]
|
|
71
|
+
if (h) return { fn: h, consumed: 3 }
|
|
72
|
+
}
|
|
73
|
+
const direct = handlers[resource]
|
|
74
|
+
if (direct) return { fn: direct, consumed: 2 }
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
36
78
|
function readPackageVersion(): string {
|
|
37
79
|
if (VERSION !== "__VERSION__") return VERSION
|
|
38
|
-
// Walk up from this file: dist/cli/index.js → dist/cli → dist → package.
|
|
39
80
|
try {
|
|
40
81
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
82
|
const { version } = require("../../package.json") as { version: string }
|
|
@@ -50,31 +91,53 @@ function showHelp(): void {
|
|
|
50
91
|
process.stdout.write(
|
|
51
92
|
`\nsentroy ${v} — Sentroy CLI\n\n` +
|
|
52
93
|
`USAGE\n` +
|
|
53
|
-
` sentroy <
|
|
54
|
-
`
|
|
55
|
-
` env push [<file>]
|
|
56
|
-
` env pull [<file>]
|
|
57
|
-
` env list
|
|
58
|
-
` env diff [<file>]
|
|
94
|
+
` sentroy <group> <subcommand> [args] [flags]\n\n` +
|
|
95
|
+
`ENV (vault sync — requires SENTROY_ENV_API_KEY)\n` +
|
|
96
|
+
` sentroy env push [<file>] ${ENV_SUBCOMMANDS.push!.description}\n` +
|
|
97
|
+
` sentroy env pull [<file>] ${ENV_SUBCOMMANDS.pull!.description}\n` +
|
|
98
|
+
` sentroy env list ${ENV_SUBCOMMANDS.list!.description}\n` +
|
|
99
|
+
` sentroy env diff [<file>] ${ENV_SUBCOMMANDS.diff!.description}\n\n` +
|
|
100
|
+
`MAIL (requires SENTROY_API_KEY + SENTROY_COMPANY_SLUG)\n` +
|
|
101
|
+
` sentroy mail templates list List email templates\n` +
|
|
102
|
+
` sentroy mail templates get <id> Template detail\n` +
|
|
103
|
+
` sentroy mail domains list List sending domains\n` +
|
|
104
|
+
` sentroy mail mailboxes list List inbox mailboxes\n` +
|
|
105
|
+
` sentroy mail inbox list Recent inbox messages (--mailbox, --folder, --unread, --q, --page, --limit)\n` +
|
|
106
|
+
` sentroy mail suppressions list Bounce/complaint suppressions (--domain, --reason)\n` +
|
|
107
|
+
` sentroy mail logs list Delivery logs (--status, --domain, --from, --to)\n` +
|
|
108
|
+
` sentroy mail logs get <id> Log detail (event timeline)\n` +
|
|
109
|
+
` sentroy mail webhooks list Webhook endpoints (--domain)\n` +
|
|
110
|
+
` sentroy mail analytics Aggregate stats (--days=7|30|90, --domain)\n\n` +
|
|
111
|
+
`STORAGE (requires SENTROY_API_KEY + SENTROY_COMPANY_SLUG)\n` +
|
|
112
|
+
` sentroy storage buckets list List storage buckets\n` +
|
|
113
|
+
` sentroy storage buckets get <slug> Bucket detail\n` +
|
|
114
|
+
` sentroy storage media list <bucketSlug> List media in bucket (--type, --folder, --q, --sort, --dir, --limit, --skip)\n` +
|
|
115
|
+
` sentroy storage media get <bucketSlug> <mediaId> Media detail\n` +
|
|
116
|
+
` sentroy storage usage Quota + per-bucket breakdown\n` +
|
|
117
|
+
` sentroy storage quota Lightweight quota only\n\n` +
|
|
118
|
+
`AI SKILL (install the Sentroy AI agent skill into your project)\n` +
|
|
119
|
+
` sentroy ai install Autodetect tools (Claude, Cursor, Windsurf) + AGENTS.md\n` +
|
|
120
|
+
` sentroy ai install --claude Install only into .claude/skills/sentroy/\n` +
|
|
121
|
+
` sentroy ai install --cursor Install only into .cursor/rules/sentroy.mdc\n` +
|
|
122
|
+
` sentroy ai install --windsurf Merge into .windsurfrules\n` +
|
|
123
|
+
` sentroy ai install --agents Merge into AGENTS.md at cwd\n` +
|
|
124
|
+
` sentroy ai install --all Install into every supported target\n` +
|
|
125
|
+
` sentroy ai install --upgrade Re-install latest, replacing existing sentinel block\n` +
|
|
126
|
+
` sentroy ai install --check Dry-run, show what would change\n\n` +
|
|
59
127
|
`GLOBAL FLAGS\n` +
|
|
60
|
-
` --token=
|
|
61
|
-
` --url=https://...
|
|
62
|
-
`
|
|
63
|
-
` --
|
|
64
|
-
` --dry-run Print the diff but do not write\n` +
|
|
65
|
-
` --yes Skip the delete-confirmation prompt (CI-friendly)\n\n` +
|
|
66
|
-
`ENV PULL FLAGS\n` +
|
|
67
|
-
` --force Overwrite the file if it already exists\n\n` +
|
|
68
|
-
`ENV LIST FLAGS\n` +
|
|
69
|
-
` --values Include values (default: keys only)\n` +
|
|
70
|
-
` --public-only Only variables marked public\n\n` +
|
|
128
|
+
` --token=stk_... Override $SENTROY_API_KEY (env vault uses $SENTROY_ENV_API_KEY)\n` +
|
|
129
|
+
` --url=https://... Override base URL (default https://sentroy.com)\n` +
|
|
130
|
+
` --company-slug=<slug> Override $SENTROY_COMPANY_SLUG (required for mail/storage)\n` +
|
|
131
|
+
` --output=json|table Output format (default table)\n\n` +
|
|
71
132
|
`EXAMPLES\n` +
|
|
72
133
|
` sentroy env push .env.production --delete-missing\n` +
|
|
73
|
-
` sentroy
|
|
74
|
-
` sentroy
|
|
75
|
-
` sentroy
|
|
76
|
-
`
|
|
77
|
-
`
|
|
134
|
+
` sentroy mail templates list --output=json | jq '.[].name'\n` +
|
|
135
|
+
` sentroy mail logs list --status=bounced --days=7\n` +
|
|
136
|
+
` sentroy storage buckets list\n` +
|
|
137
|
+
` sentroy ai install\n\n` +
|
|
138
|
+
`DOCS\n` +
|
|
139
|
+
` https://docs.sentroy.com/cli Full CLI reference\n` +
|
|
140
|
+
` https://docs.sentroy.com/ai-skills AI Skill install guide\n\n`,
|
|
78
141
|
)
|
|
79
142
|
}
|
|
80
143
|
|
|
@@ -95,6 +158,7 @@ async function main(): Promise<void> {
|
|
|
95
158
|
}
|
|
96
159
|
|
|
97
160
|
const cmd = argv[0]
|
|
161
|
+
|
|
98
162
|
if (cmd === "env") {
|
|
99
163
|
const sub = argv[1]
|
|
100
164
|
const handler = sub ? ENV_SUBCOMMANDS[sub] : undefined
|
|
@@ -109,6 +173,64 @@ async function main(): Promise<void> {
|
|
|
109
173
|
return
|
|
110
174
|
}
|
|
111
175
|
|
|
176
|
+
if (cmd === "mail") {
|
|
177
|
+
// `mail <resource> [verb] [args]` → handler key = resource + Capitalize(verb).
|
|
178
|
+
// Exception: `mail analytics` has no verb → handler key = "analytics".
|
|
179
|
+
const resource = argv[1]
|
|
180
|
+
const verb = argv[2]
|
|
181
|
+
if (!resource) {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`usage: sentroy mail <resource> [verb] [args]\nresources: templates, domains, mailboxes, inbox, suppressions, logs, webhooks, analytics\n`,
|
|
184
|
+
)
|
|
185
|
+
process.exit(1)
|
|
186
|
+
}
|
|
187
|
+
const handler = resolveHandler(MAIL_HANDLERS, resource, verb)
|
|
188
|
+
if (!handler) {
|
|
189
|
+
process.stderr.write(
|
|
190
|
+
`unknown mail command: \`sentroy mail ${resource}${verb ? " " + verb : ""}\`\n` +
|
|
191
|
+
`available: ${Object.keys(MAIL_HANDLERS).join(", ")}\n`,
|
|
192
|
+
)
|
|
193
|
+
process.exit(1)
|
|
194
|
+
}
|
|
195
|
+
await handler.fn(argv.slice(handler.consumed))
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (cmd === "storage") {
|
|
200
|
+
const resource = argv[1]
|
|
201
|
+
const verb = argv[2]
|
|
202
|
+
if (!resource) {
|
|
203
|
+
process.stderr.write(
|
|
204
|
+
`usage: sentroy storage <resource> [verb] [args]\nresources: buckets, media, usage, quota\n`,
|
|
205
|
+
)
|
|
206
|
+
process.exit(1)
|
|
207
|
+
}
|
|
208
|
+
const handler = resolveHandler(STORAGE_HANDLERS, resource, verb)
|
|
209
|
+
if (!handler) {
|
|
210
|
+
process.stderr.write(
|
|
211
|
+
`unknown storage command: \`sentroy storage ${resource}${verb ? " " + verb : ""}\`\n` +
|
|
212
|
+
`available: ${Object.keys(STORAGE_HANDLERS).join(", ")}\n`,
|
|
213
|
+
)
|
|
214
|
+
process.exit(1)
|
|
215
|
+
}
|
|
216
|
+
await handler.fn(argv.slice(handler.consumed))
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (cmd === "ai") {
|
|
221
|
+
const sub = argv[1]
|
|
222
|
+
const handler = sub ? AI_HANDLERS[sub as keyof typeof AI_HANDLERS] : undefined
|
|
223
|
+
if (!handler) {
|
|
224
|
+
process.stderr.write(
|
|
225
|
+
`unknown ai subcommand: ${sub ?? "<missing>"}\n` +
|
|
226
|
+
`available: ${Object.keys(AI_HANDLERS).join(", ")}\n`,
|
|
227
|
+
)
|
|
228
|
+
process.exit(1)
|
|
229
|
+
}
|
|
230
|
+
await handler(argv.slice(2))
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
112
234
|
process.stderr.write(
|
|
113
235
|
`unknown command: ${cmd}\nrun \`sentroy --help\` for usage.\n`,
|
|
114
236
|
)
|