@beaulewis/saas-cli 1.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.
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/bin/saas.js +2 -0
- package/dist/chunk-26VE6QJ4.js +120 -0
- package/dist/chunk-26VE6QJ4.js.map +1 -0
- package/dist/chunk-3KD5CFV3.js +196 -0
- package/dist/chunk-3KD5CFV3.js.map +1 -0
- package/dist/chunk-5BCEXHNM.js +108 -0
- package/dist/chunk-5BCEXHNM.js.map +1 -0
- package/dist/chunk-N4OIAZSA.js +110 -0
- package/dist/chunk-N4OIAZSA.js.map +1 -0
- package/dist/chunk-ZD2ZSBK3.js +224 -0
- package/dist/chunk-ZD2ZSBK3.js.map +1 -0
- package/dist/dart-DXLFNGHR.js +41 -0
- package/dist/dart-DXLFNGHR.js.map +1 -0
- package/dist/drift-XYY4D366.js +59 -0
- package/dist/drift-XYY4D366.js.map +1 -0
- package/dist/flutter-J5BYPVIW.js +41 -0
- package/dist/flutter-J5BYPVIW.js.map +1 -0
- package/dist/freezed-QXFQ4GJC.js +58 -0
- package/dist/freezed-QXFQ4GJC.js.map +1 -0
- package/dist/gorouter-QBMTTFVR.js +56 -0
- package/dist/gorouter-QBMTTFVR.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1437 -0
- package/dist/index.js.map +1 -0
- package/dist/package-QO75XHBD.js +43 -0
- package/dist/package-QO75XHBD.js.map +1 -0
- package/dist/powersync-I3LR7TDN.js +37 -0
- package/dist/powersync-I3LR7TDN.js.map +1 -0
- package/dist/repository-BAOVD3NG.js +34 -0
- package/dist/repository-BAOVD3NG.js.map +1 -0
- package/dist/riverpod-XUU656PM.js +42 -0
- package/dist/riverpod-XUU656PM.js.map +1 -0
- package/dist/widget-YDKHPRXM.js +42 -0
- package/dist/widget-YDKHPRXM.js.map +1 -0
- package/package.json +89 -0
- package/templates/drift/dao.hbs +51 -0
- package/templates/drift/migration.hbs +15 -0
- package/templates/freezed/model.hbs +20 -0
- package/templates/gorouter/route.hbs +18 -0
- package/templates/powersync/rules.hbs +10 -0
- package/templates/powersync/schema.hbs +19 -0
- package/templates/repository/repository.hbs +62 -0
- package/templates/riverpod/async-notifier.hbs +44 -0
- package/templates/riverpod/family.hbs +9 -0
- package/templates/riverpod/future.hbs +9 -0
- package/templates/riverpod/notifier.hbs +34 -0
- package/templates/riverpod/stream.hbs +9 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1437 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cacheKey,
|
|
3
|
+
cachedFetch,
|
|
4
|
+
createAuthenticatedClient,
|
|
5
|
+
formatTable,
|
|
6
|
+
getAPIKey,
|
|
7
|
+
loadGlobalConfig
|
|
8
|
+
} from "./chunk-3KD5CFV3.js";
|
|
9
|
+
import {
|
|
10
|
+
columnsToSQL,
|
|
11
|
+
parseColumnSpec
|
|
12
|
+
} from "./chunk-ZD2ZSBK3.js";
|
|
13
|
+
import {
|
|
14
|
+
AuthError,
|
|
15
|
+
CLIError,
|
|
16
|
+
MODEL_ALIASES,
|
|
17
|
+
handleError
|
|
18
|
+
} from "./chunk-5BCEXHNM.js";
|
|
19
|
+
|
|
20
|
+
// src/cli.ts
|
|
21
|
+
import { Command as Command11 } from "commander";
|
|
22
|
+
import pc8 from "picocolors";
|
|
23
|
+
|
|
24
|
+
// src/commands/ask/index.ts
|
|
25
|
+
import { Command } from "commander";
|
|
26
|
+
import ora from "ora";
|
|
27
|
+
import pc from "picocolors";
|
|
28
|
+
|
|
29
|
+
// src/services/perplexity.ts
|
|
30
|
+
var PERPLEXITY_BASE_URL = "https://api.perplexity.ai";
|
|
31
|
+
var PerplexityClient = class {
|
|
32
|
+
client;
|
|
33
|
+
constructor(apiKey) {
|
|
34
|
+
this.client = createAuthenticatedClient(PERPLEXITY_BASE_URL, apiKey);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Ask a question using Perplexity AI
|
|
38
|
+
*/
|
|
39
|
+
async ask(query, options = {}) {
|
|
40
|
+
const model = options.model ?? "sonar";
|
|
41
|
+
const requestBody = {
|
|
42
|
+
model,
|
|
43
|
+
messages: [
|
|
44
|
+
{
|
|
45
|
+
role: "system",
|
|
46
|
+
content: "You are a helpful assistant for software development questions. Be precise and include code examples when relevant. Focus on Flutter, Dart, and related technologies."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
role: "user",
|
|
50
|
+
content: query
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
max_tokens: options.maxTokens ?? 2048,
|
|
54
|
+
temperature: options.temperature ?? 0.2,
|
|
55
|
+
return_related_questions: false
|
|
56
|
+
};
|
|
57
|
+
if (options.searchRecencyFilter) {
|
|
58
|
+
requestBody.search_recency_filter = options.searchRecencyFilter;
|
|
59
|
+
}
|
|
60
|
+
if (options.searchDomainFilter && options.searchDomainFilter.length > 0) {
|
|
61
|
+
requestBody.search_domain_filter = options.searchDomainFilter;
|
|
62
|
+
}
|
|
63
|
+
const response = await this.client.post("chat/completions", {
|
|
64
|
+
json: requestBody
|
|
65
|
+
}).json();
|
|
66
|
+
return this.formatResponse(response, options.returnSources);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Ask with caching enabled
|
|
70
|
+
*/
|
|
71
|
+
async askCached(query, options = {}) {
|
|
72
|
+
const model = options.model ?? "sonar";
|
|
73
|
+
const key = cacheKey("perplexity", model, query);
|
|
74
|
+
return cachedFetch(
|
|
75
|
+
key,
|
|
76
|
+
() => this.ask(query, options),
|
|
77
|
+
1800 * 1e3
|
|
78
|
+
// 30 minute cache for AI responses
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Format the API response
|
|
83
|
+
*/
|
|
84
|
+
formatResponse(response, includeSources) {
|
|
85
|
+
const choice = response.choices[0];
|
|
86
|
+
if (!choice) {
|
|
87
|
+
return "No response received from AI.";
|
|
88
|
+
}
|
|
89
|
+
let output = choice.message.content;
|
|
90
|
+
if (includeSources && response.citations && response.citations.length > 0) {
|
|
91
|
+
output += "\n\n---\nSources:\n";
|
|
92
|
+
response.citations.forEach((url, i) => {
|
|
93
|
+
output += `[${i + 1}] ${url}
|
|
94
|
+
`;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return output;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var clientInstance = null;
|
|
101
|
+
var currentApiKey = null;
|
|
102
|
+
function getPerplexityClient(apiKey) {
|
|
103
|
+
if (!clientInstance || currentApiKey !== apiKey) {
|
|
104
|
+
clientInstance = new PerplexityClient(apiKey);
|
|
105
|
+
currentApiKey = apiKey;
|
|
106
|
+
}
|
|
107
|
+
return clientInstance;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/commands/ask/index.ts
|
|
111
|
+
var askCommand = new Command("ask").description("Ask AI-powered questions with web search (Perplexity)").argument("<query>", "Your question").option("-m, --model <model>", "Model: sonar, pro, reasoning, deep", "sonar").option("--recent <period>", "Filter by recency: day, week, month, year").option("--domains <domains>", "Limit to domains (comma-separated)").option("--sources", "Include source URLs in output").option("--json", "Output as JSON").action(async (query, options) => {
|
|
112
|
+
const modelAlias = options.model ?? "sonar";
|
|
113
|
+
const model = MODEL_ALIASES[modelAlias] ?? "sonar";
|
|
114
|
+
const modelDisplay = modelAlias.toUpperCase();
|
|
115
|
+
const spinner = ora(`Asking ${modelDisplay}...`).start();
|
|
116
|
+
try {
|
|
117
|
+
const config = await loadGlobalConfig();
|
|
118
|
+
const apiKey = getAPIKey("perplexity", config);
|
|
119
|
+
if (!apiKey) {
|
|
120
|
+
spinner.fail("Perplexity API key not found");
|
|
121
|
+
throw new AuthError(
|
|
122
|
+
"PERPLEXITY_API_KEY environment variable is not set",
|
|
123
|
+
"Get your API key from https://perplexity.ai/settings/api and set PERPLEXITY_API_KEY"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const client = getPerplexityClient(apiKey);
|
|
127
|
+
const result = await client.ask(query, {
|
|
128
|
+
model,
|
|
129
|
+
searchRecencyFilter: options.recent,
|
|
130
|
+
searchDomainFilter: options.domains?.split(","),
|
|
131
|
+
returnSources: options.sources
|
|
132
|
+
});
|
|
133
|
+
spinner.stop();
|
|
134
|
+
if (options.json) {
|
|
135
|
+
console.log(JSON.stringify({ query, model: modelAlias, result }, null, 2));
|
|
136
|
+
} else {
|
|
137
|
+
console.log(pc.cyan(`
|
|
138
|
+
\u2501\u2501\u2501 ${modelDisplay} \u2501\u2501\u2501
|
|
139
|
+
`));
|
|
140
|
+
console.log(result);
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
spinner.fail("Query failed");
|
|
144
|
+
handleError(error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// src/commands/cf/index.ts
|
|
149
|
+
import { exec } from "child_process";
|
|
150
|
+
import { promisify } from "util";
|
|
151
|
+
import { Command as Command2 } from "commander";
|
|
152
|
+
import ora2 from "ora";
|
|
153
|
+
var execAsync = promisify(exec);
|
|
154
|
+
async function checkWranglerCLI() {
|
|
155
|
+
try {
|
|
156
|
+
await execAsync("wrangler --version");
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function runWranglerCommand(args) {
|
|
163
|
+
const isInstalled = await checkWranglerCLI();
|
|
164
|
+
if (!isInstalled) {
|
|
165
|
+
throw new CLIError("Wrangler CLI not found", 1, "Install with: pnpm add -g wrangler");
|
|
166
|
+
}
|
|
167
|
+
if (!process.env.CF_API_TOKEN && !process.env.CLOUDFLARE_API_TOKEN) {
|
|
168
|
+
throw new AuthError(
|
|
169
|
+
"Cloudflare API token not found",
|
|
170
|
+
"Set CF_API_TOKEN or CLOUDFLARE_API_TOKEN environment variable"
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const { stdout, stderr } = await execAsync(`wrangler ${args.join(" ")}`);
|
|
175
|
+
if (stderr && !stderr.includes("warning")) {
|
|
176
|
+
console.error(stderr);
|
|
177
|
+
}
|
|
178
|
+
return stdout;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
+
throw new CLIError(`Wrangler command failed: ${message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
var cfCommand = new Command2("cf").description("Cloudflare Workers and KV management").addCommand(
|
|
185
|
+
new Command2("worker").description("Worker management").argument("<action>", "Action: new, deploy, logs, list").argument("[name]", "Worker name").action(async (action, name) => {
|
|
186
|
+
const spinner = ora2(`Worker ${action}...`).start();
|
|
187
|
+
try {
|
|
188
|
+
let args;
|
|
189
|
+
switch (action) {
|
|
190
|
+
case "new":
|
|
191
|
+
if (!name) throw new CLIError('Worker name required for "new" action');
|
|
192
|
+
args = ["init", name];
|
|
193
|
+
break;
|
|
194
|
+
case "deploy":
|
|
195
|
+
args = name ? ["deploy", "--name", name] : ["deploy"];
|
|
196
|
+
break;
|
|
197
|
+
case "logs":
|
|
198
|
+
args = name ? ["tail", name] : ["tail"];
|
|
199
|
+
break;
|
|
200
|
+
case "list":
|
|
201
|
+
args = ["deployments", "list"];
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
throw new CLIError(
|
|
205
|
+
`Invalid action: "${action}"`,
|
|
206
|
+
1,
|
|
207
|
+
"Valid actions: new, deploy, logs, list"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const output = await runWranglerCommand(args);
|
|
211
|
+
spinner.stop();
|
|
212
|
+
console.log(output);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
spinner.fail(`Worker ${action} failed`);
|
|
215
|
+
handleError(error);
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
).addCommand(
|
|
219
|
+
new Command2("kv").description("KV namespace operations").argument("<action>", "Action: namespaces, keys, get, put, del").argument("[namespace]", "KV namespace ID").argument("[key]", "Key name").argument("[value]", "Value (for put)").action(async (action, namespace, key, value) => {
|
|
220
|
+
const spinner = ora2(`KV ${action}...`).start();
|
|
221
|
+
try {
|
|
222
|
+
let args;
|
|
223
|
+
switch (action) {
|
|
224
|
+
case "namespaces":
|
|
225
|
+
args = ["kv:namespace", "list"];
|
|
226
|
+
break;
|
|
227
|
+
case "keys":
|
|
228
|
+
if (!namespace) throw new CLIError("Namespace ID required");
|
|
229
|
+
args = ["kv:key", "list", "--namespace-id", namespace];
|
|
230
|
+
break;
|
|
231
|
+
case "get":
|
|
232
|
+
if (!namespace || !key) throw new CLIError("Namespace and key required");
|
|
233
|
+
args = ["kv:key", "get", key, "--namespace-id", namespace];
|
|
234
|
+
break;
|
|
235
|
+
case "put":
|
|
236
|
+
if (!namespace || !key || !value)
|
|
237
|
+
throw new CLIError("Namespace, key, and value required");
|
|
238
|
+
args = ["kv:key", "put", key, value, "--namespace-id", namespace];
|
|
239
|
+
break;
|
|
240
|
+
case "del":
|
|
241
|
+
if (!namespace || !key) throw new CLIError("Namespace and key required");
|
|
242
|
+
args = ["kv:key", "delete", key, "--namespace-id", namespace];
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
throw new CLIError(
|
|
246
|
+
`Invalid action: "${action}"`,
|
|
247
|
+
1,
|
|
248
|
+
"Valid actions: namespaces, keys, get, put, del"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const output = await runWranglerCommand(args);
|
|
252
|
+
spinner.stop();
|
|
253
|
+
console.log(output);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
spinner.fail(`KV ${action} failed`);
|
|
256
|
+
handleError(error);
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// src/commands/docs/index.ts
|
|
262
|
+
import { Command as Command3 } from "commander";
|
|
263
|
+
var docsCommand = new Command3("docs").description("Look up documentation via Context7").addCommand(
|
|
264
|
+
new Command3("flutter").description("Search Flutter documentation").argument("<query>", "Search query").action(async (query) => {
|
|
265
|
+
const { flutterAction } = await import("./flutter-J5BYPVIW.js");
|
|
266
|
+
await flutterAction(query);
|
|
267
|
+
})
|
|
268
|
+
).addCommand(
|
|
269
|
+
new Command3("dart").description("Search Dart documentation").argument("<query>", "Search query").action(async (query) => {
|
|
270
|
+
const { dartAction } = await import("./dart-DXLFNGHR.js");
|
|
271
|
+
await dartAction(query);
|
|
272
|
+
})
|
|
273
|
+
).addCommand(
|
|
274
|
+
new Command3("package").description("Search package documentation").argument("<package>", "Package name").argument("[query]", "Search query").action(async (pkg, query) => {
|
|
275
|
+
const { packageAction } = await import("./package-QO75XHBD.js");
|
|
276
|
+
await packageAction(pkg, query);
|
|
277
|
+
})
|
|
278
|
+
).addCommand(
|
|
279
|
+
new Command3("widget").description("Look up widget documentation").argument("<name>", "Widget name").option("--properties", "Show all properties").action(async (name, options) => {
|
|
280
|
+
const { widgetAction } = await import("./widget-YDKHPRXM.js");
|
|
281
|
+
await widgetAction(name, options);
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// src/commands/flags/index.ts
|
|
286
|
+
import { Command as Command4 } from "commander";
|
|
287
|
+
import got from "got";
|
|
288
|
+
import ora3 from "ora";
|
|
289
|
+
import pc2 from "picocolors";
|
|
290
|
+
var POSTHOG_API = "https://app.posthog.com/api";
|
|
291
|
+
async function getPostHogConfig() {
|
|
292
|
+
const projectId = process.env.POSTHOG_PROJECT_ID;
|
|
293
|
+
const apiKey = process.env.POSTHOG_API_KEY;
|
|
294
|
+
if (!projectId || !apiKey) {
|
|
295
|
+
const config = await loadGlobalConfig();
|
|
296
|
+
return {
|
|
297
|
+
projectId: projectId || config.posthog?.projectId,
|
|
298
|
+
apiKey: apiKey || config.posthog?.apiKey
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return { projectId, apiKey };
|
|
302
|
+
}
|
|
303
|
+
var flagsCommand = new Command4("flags").description("PostHog feature flag management").addCommand(
|
|
304
|
+
new Command4("list").description("List all feature flags").action(async () => {
|
|
305
|
+
const spinner = ora3("Fetching feature flags...").start();
|
|
306
|
+
try {
|
|
307
|
+
const { projectId, apiKey } = await getPostHogConfig();
|
|
308
|
+
if (!projectId || !apiKey) {
|
|
309
|
+
throw new AuthError(
|
|
310
|
+
"PostHog credentials not found",
|
|
311
|
+
"Set POSTHOG_PROJECT_ID and POSTHOG_API_KEY environment variables"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const response = await got.get(`${POSTHOG_API}/projects/${projectId}/feature_flags`, {
|
|
315
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
316
|
+
}).json();
|
|
317
|
+
spinner.stop();
|
|
318
|
+
const rows = response.results.map((flag) => [
|
|
319
|
+
flag.key,
|
|
320
|
+
flag.active ? pc2.green("enabled") : pc2.red("disabled"),
|
|
321
|
+
flag.rollout_percentage !== void 0 ? `${flag.rollout_percentage}%` : "100%"
|
|
322
|
+
]);
|
|
323
|
+
console.log(formatTable(["Flag", "Status", "Rollout"], rows));
|
|
324
|
+
} catch (error) {
|
|
325
|
+
spinner.fail("Failed to fetch feature flags");
|
|
326
|
+
handleError(error);
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
).addCommand(
|
|
330
|
+
new Command4("get").description("Get feature flag details").argument("<flag>", "Flag key").action(async (flag) => {
|
|
331
|
+
const spinner = ora3(`Fetching flag: ${flag}...`).start();
|
|
332
|
+
try {
|
|
333
|
+
const { projectId, apiKey } = await getPostHogConfig();
|
|
334
|
+
if (!projectId || !apiKey) {
|
|
335
|
+
throw new AuthError(
|
|
336
|
+
"PostHog credentials not found",
|
|
337
|
+
"Set POSTHOG_PROJECT_ID and POSTHOG_API_KEY environment variables"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const response = await got.get(`${POSTHOG_API}/projects/${projectId}/feature_flags`, {
|
|
341
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
342
|
+
searchParams: { key: flag }
|
|
343
|
+
}).json();
|
|
344
|
+
spinner.stop();
|
|
345
|
+
const flagData = response.results.find((f) => f.key === flag);
|
|
346
|
+
if (!flagData) {
|
|
347
|
+
console.log(pc2.yellow(`Flag "${flag}" not found`));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
console.log(pc2.cyan(`Flag: ${flagData.key}`));
|
|
351
|
+
console.log(` Name: ${flagData.name || "N/A"}`);
|
|
352
|
+
console.log(` Status: ${flagData.active ? pc2.green("enabled") : pc2.red("disabled")}`);
|
|
353
|
+
console.log(` Rollout: ${flagData.rollout_percentage ?? 100}%`);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
spinner.fail("Failed to fetch flag");
|
|
356
|
+
handleError(error);
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
).addCommand(
|
|
360
|
+
new Command4("set").description("Enable or disable a feature flag").argument("<flag>", "Flag key").argument("<value>", "true/false or percentage (0-100)").action(async (flag, value) => {
|
|
361
|
+
const spinner = ora3(`Updating flag: ${flag}...`).start();
|
|
362
|
+
try {
|
|
363
|
+
const { projectId, apiKey } = await getPostHogConfig();
|
|
364
|
+
if (!projectId || !apiKey) {
|
|
365
|
+
throw new AuthError(
|
|
366
|
+
"PostHog credentials not found",
|
|
367
|
+
"Set POSTHOG_PROJECT_ID and POSTHOG_API_KEY environment variables"
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const listResponse = await got.get(`${POSTHOG_API}/projects/${projectId}/feature_flags`, {
|
|
371
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
372
|
+
}).json();
|
|
373
|
+
const flagData = listResponse.results.find((f) => f.key === flag);
|
|
374
|
+
if (!flagData) {
|
|
375
|
+
spinner.fail(`Flag "${flag}" not found`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const isPercentage = /^\d+$/.test(value) && Number.parseInt(value, 10) <= 100;
|
|
379
|
+
const isBoolean = value === "true" || value === "false";
|
|
380
|
+
const updateData = {};
|
|
381
|
+
if (isPercentage) {
|
|
382
|
+
updateData.rollout_percentage = Number.parseInt(value, 10);
|
|
383
|
+
} else if (isBoolean) {
|
|
384
|
+
updateData.active = value === "true";
|
|
385
|
+
} else {
|
|
386
|
+
spinner.fail("Value must be true, false, or a percentage (0-100)");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
await got.patch(`${POSTHOG_API}/projects/${projectId}/feature_flags/${flagData.id}`, {
|
|
390
|
+
headers: {
|
|
391
|
+
Authorization: `Bearer ${apiKey}`,
|
|
392
|
+
"Content-Type": "application/json"
|
|
393
|
+
},
|
|
394
|
+
json: updateData
|
|
395
|
+
});
|
|
396
|
+
spinner.succeed("Feature flag updated");
|
|
397
|
+
console.log(pc2.green(`${flag} = ${value}`));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
spinner.fail("Failed to update flag");
|
|
400
|
+
handleError(error);
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
).addCommand(
|
|
404
|
+
new Command4("add-user").description("Add user to flag override").argument("<flag>", "Flag key").argument("<userId>", "User ID").action(async (flag, userId) => {
|
|
405
|
+
console.log(pc2.yellow("Note: User-level overrides require PostHog Enterprise"));
|
|
406
|
+
console.log(pc2.dim(`Would add user ${userId} to flag ${flag}`));
|
|
407
|
+
})
|
|
408
|
+
).addCommand(
|
|
409
|
+
new Command4("remove-user").description("Remove user from flag override").argument("<flag>", "Flag key").argument("<userId>", "User ID").action(async (flag, userId) => {
|
|
410
|
+
console.log(pc2.yellow("Note: User-level overrides require PostHog Enterprise"));
|
|
411
|
+
console.log(pc2.dim(`Would remove user ${userId} from flag ${flag}`));
|
|
412
|
+
})
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// src/commands/gen/index.ts
|
|
416
|
+
import { Command as Command5 } from "commander";
|
|
417
|
+
var genCommand = new Command5("gen").description("Generate boilerplate code for Flutter/Dart patterns").addCommand(
|
|
418
|
+
new Command5("riverpod").description("Generate Riverpod providers").argument("<pattern>", "Pattern: notifier, async-notifier, future, stream, family").argument("<name>", "Provider name").option("-s, --state <type>", 'State type (e.g., "List<User>")').option("-o, --output <path>", "Output file path").action(async (pattern, name, options) => {
|
|
419
|
+
const { riverpodAction } = await import("./riverpod-XUU656PM.js");
|
|
420
|
+
await riverpodAction(pattern, name, options);
|
|
421
|
+
})
|
|
422
|
+
).addCommand(
|
|
423
|
+
new Command5("drift").description("Generate Drift database code").argument("<type>", "Type: table, dao, migration").argument("<name>", "Table or DAO name").option("-c, --columns <spec>", "Column specification (for table)").option("-o, --output <path>", "Output file path").action(async (type, name, options) => {
|
|
424
|
+
const { driftAction } = await import("./drift-XYY4D366.js");
|
|
425
|
+
await driftAction(type, name, options);
|
|
426
|
+
})
|
|
427
|
+
).addCommand(
|
|
428
|
+
new Command5("gorouter").description("Generate GoRouter routes").argument("<name>", "Route name").option("-p, --path <path>", "Route path (e.g., /recipe/:id)").option("--params <params>", "Path parameters (e.g., id:string)").option("-o, --output <path>", "Output file path").action(async (name, options) => {
|
|
429
|
+
const { gorouterAction } = await import("./gorouter-QBMTTFVR.js");
|
|
430
|
+
await gorouterAction(name, options);
|
|
431
|
+
})
|
|
432
|
+
).addCommand(
|
|
433
|
+
new Command5("powersync").description("Generate PowerSync sync rules").argument("<type>", "Type: rules, schema").argument("<table>", "Table name").option("-u, --user-column <column>", "User ID column for sync rules").option("-o, --output <path>", "Output file path").action(async (type, table, options) => {
|
|
434
|
+
const { powersyncAction } = await import("./powersync-I3LR7TDN.js");
|
|
435
|
+
await powersyncAction(type, table, options);
|
|
436
|
+
})
|
|
437
|
+
).addCommand(
|
|
438
|
+
new Command5("freezed").description("Generate Freezed models").argument("<name>", "Model name").option(
|
|
439
|
+
"-f, --fields <spec>",
|
|
440
|
+
"Field specification (e.g., id:String,name:String,email:String?)"
|
|
441
|
+
).option("-o, --output <path>", "Output file path").action(async (name, options) => {
|
|
442
|
+
const { freezedAction } = await import("./freezed-QXFQ4GJC.js");
|
|
443
|
+
await freezedAction(name, options);
|
|
444
|
+
})
|
|
445
|
+
).addCommand(
|
|
446
|
+
new Command5("repository").description("Generate repository pattern").argument("<name>", "Repository name").option("-e, --entity <name>", "Entity name").option("-o, --output <path>", "Output file path").action(async (name, options) => {
|
|
447
|
+
const { repositoryAction } = await import("./repository-BAOVD3NG.js");
|
|
448
|
+
await repositoryAction(name, options);
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// src/commands/init/index.ts
|
|
453
|
+
import { existsSync } from "fs";
|
|
454
|
+
import { writeFile } from "fs/promises";
|
|
455
|
+
import { Command as Command6 } from "commander";
|
|
456
|
+
import ora4 from "ora";
|
|
457
|
+
import pc3 from "picocolors";
|
|
458
|
+
var initCommand = new Command6("init").description("Initialize projects and add features").addCommand(
|
|
459
|
+
new Command6("flutter").description("Initialize a new Flutter project with SaaS stack").argument("<name>", "Project name").action(async (name) => {
|
|
460
|
+
const spinner = ora4("Creating Flutter project...").start();
|
|
461
|
+
try {
|
|
462
|
+
const { exec: exec4 } = await import("child_process");
|
|
463
|
+
const { promisify: promisify4 } = await import("util");
|
|
464
|
+
const execAsync4 = promisify4(exec4);
|
|
465
|
+
await execAsync4(`flutter create ${name}`);
|
|
466
|
+
spinner.text = "Adding dependencies...";
|
|
467
|
+
const deps = [
|
|
468
|
+
"flutter_riverpod",
|
|
469
|
+
"riverpod_annotation",
|
|
470
|
+
"go_router",
|
|
471
|
+
"freezed_annotation",
|
|
472
|
+
"json_annotation",
|
|
473
|
+
"supabase_flutter"
|
|
474
|
+
];
|
|
475
|
+
const devDeps = ["riverpod_generator", "build_runner", "freezed", "json_serializable"];
|
|
476
|
+
process.chdir(name);
|
|
477
|
+
await execAsync4(`flutter pub add ${deps.join(" ")}`);
|
|
478
|
+
await execAsync4(`flutter pub add --dev ${devDeps.join(" ")}`);
|
|
479
|
+
spinner.succeed("Flutter project created");
|
|
480
|
+
console.log(pc3.green(`
|
|
481
|
+
Project "${name}" created with:`));
|
|
482
|
+
console.log(" - Riverpod (state management)");
|
|
483
|
+
console.log(" - GoRouter (navigation)");
|
|
484
|
+
console.log(" - Freezed (immutable models)");
|
|
485
|
+
console.log(" - Supabase (backend)");
|
|
486
|
+
console.log(`
|
|
487
|
+
Next steps:`);
|
|
488
|
+
console.log(` cd ${name}`);
|
|
489
|
+
console.log(` flutter run`);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
spinner.fail("Failed to create Flutter project");
|
|
492
|
+
handleError(error);
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
).addCommand(
|
|
496
|
+
new Command6("supabase").description("Initialize Supabase in current project").action(async () => {
|
|
497
|
+
const spinner = ora4("Initializing Supabase...").start();
|
|
498
|
+
try {
|
|
499
|
+
const { exec: exec4 } = await import("child_process");
|
|
500
|
+
const { promisify: promisify4 } = await import("util");
|
|
501
|
+
const execAsync4 = promisify4(exec4);
|
|
502
|
+
await execAsync4("supabase init");
|
|
503
|
+
spinner.succeed("Supabase initialized");
|
|
504
|
+
console.log(pc3.green("\nSupabase project created"));
|
|
505
|
+
console.log("Next steps:");
|
|
506
|
+
console.log(" supabase start");
|
|
507
|
+
console.log(" supabase db push");
|
|
508
|
+
} catch (error) {
|
|
509
|
+
spinner.fail("Failed to initialize Supabase");
|
|
510
|
+
handleError(error);
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
).addCommand(
|
|
514
|
+
new Command6("worker").description("Initialize a new Cloudflare Worker").argument("<name>", "Worker name").action(async (name) => {
|
|
515
|
+
const spinner = ora4("Creating Cloudflare Worker...").start();
|
|
516
|
+
try {
|
|
517
|
+
const { exec: exec4 } = await import("child_process");
|
|
518
|
+
const { promisify: promisify4 } = await import("util");
|
|
519
|
+
const execAsync4 = promisify4(exec4);
|
|
520
|
+
await execAsync4(`wrangler init ${name}`);
|
|
521
|
+
spinner.succeed("Cloudflare Worker created");
|
|
522
|
+
console.log(pc3.green(`
|
|
523
|
+
Worker "${name}" created`));
|
|
524
|
+
console.log("Next steps:");
|
|
525
|
+
console.log(` cd ${name}`);
|
|
526
|
+
console.log(" wrangler dev");
|
|
527
|
+
} catch (error) {
|
|
528
|
+
spinner.fail("Failed to create Worker");
|
|
529
|
+
handleError(error);
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
).addCommand(
|
|
533
|
+
new Command6("add").description("Add a feature to existing Flutter project").argument("<feature>", "Feature: riverpod, drift, powersync, auth").action(async (feature) => {
|
|
534
|
+
const spinner = ora4(`Adding ${feature}...`).start();
|
|
535
|
+
try {
|
|
536
|
+
const { exec: exec4 } = await import("child_process");
|
|
537
|
+
const { promisify: promisify4 } = await import("util");
|
|
538
|
+
const execAsync4 = promisify4(exec4);
|
|
539
|
+
let deps = [];
|
|
540
|
+
let devDeps = [];
|
|
541
|
+
switch (feature) {
|
|
542
|
+
case "riverpod":
|
|
543
|
+
deps = ["flutter_riverpod", "riverpod_annotation"];
|
|
544
|
+
devDeps = ["riverpod_generator", "build_runner"];
|
|
545
|
+
break;
|
|
546
|
+
case "drift":
|
|
547
|
+
deps = ["drift", "sqlite3_flutter_libs", "path_provider", "path"];
|
|
548
|
+
devDeps = ["drift_dev", "build_runner"];
|
|
549
|
+
break;
|
|
550
|
+
case "powersync":
|
|
551
|
+
deps = ["powersync"];
|
|
552
|
+
break;
|
|
553
|
+
case "auth":
|
|
554
|
+
deps = ["supabase_flutter"];
|
|
555
|
+
break;
|
|
556
|
+
default:
|
|
557
|
+
throw new CLIError(
|
|
558
|
+
`Unknown feature: "${feature}"`,
|
|
559
|
+
1,
|
|
560
|
+
"Valid features: riverpod, drift, powersync, auth"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (deps.length > 0) {
|
|
564
|
+
await execAsync4(`flutter pub add ${deps.join(" ")}`);
|
|
565
|
+
}
|
|
566
|
+
if (devDeps.length > 0) {
|
|
567
|
+
await execAsync4(`flutter pub add --dev ${devDeps.join(" ")}`);
|
|
568
|
+
}
|
|
569
|
+
spinner.succeed(`${feature} added`);
|
|
570
|
+
console.log(pc3.green(`
|
|
571
|
+
${feature} dependencies installed`));
|
|
572
|
+
if (feature === "riverpod" || feature === "drift") {
|
|
573
|
+
console.log("Run code generation:");
|
|
574
|
+
console.log(" dart run build_runner build");
|
|
575
|
+
}
|
|
576
|
+
} catch (error) {
|
|
577
|
+
spinner.fail(`Failed to add ${feature}`);
|
|
578
|
+
handleError(error);
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
).addCommand(
|
|
582
|
+
new Command6("config").description("Create saas.yaml config file").action(async () => {
|
|
583
|
+
const configPath = "saas.yaml";
|
|
584
|
+
if (existsSync(configPath)) {
|
|
585
|
+
console.log(pc3.yellow("saas.yaml already exists"));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const config = `# SaaS CLI Configuration
|
|
589
|
+
project:
|
|
590
|
+
name: my-saas-app
|
|
591
|
+
type: flutter
|
|
592
|
+
|
|
593
|
+
flutter:
|
|
594
|
+
path: .
|
|
595
|
+
|
|
596
|
+
supabase:
|
|
597
|
+
path: supabase
|
|
598
|
+
types_output: lib/generated/supabase_types.dart
|
|
599
|
+
|
|
600
|
+
templates:
|
|
601
|
+
riverpod:
|
|
602
|
+
path: lib/features/{feature}/presentation/providers
|
|
603
|
+
drift:
|
|
604
|
+
path: lib/features/{feature}/data/datasources/local
|
|
605
|
+
freezed:
|
|
606
|
+
path: lib/features/{feature}/domain/entities
|
|
607
|
+
`;
|
|
608
|
+
await writeFile(configPath, config);
|
|
609
|
+
console.log(pc3.green("\u2713 Created saas.yaml"));
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
// src/commands/push/index.ts
|
|
614
|
+
import { Command as Command7 } from "commander";
|
|
615
|
+
import got2 from "got";
|
|
616
|
+
import ora5 from "ora";
|
|
617
|
+
import pc4 from "picocolors";
|
|
618
|
+
var ONESIGNAL_API = "https://onesignal.com/api/v1";
|
|
619
|
+
async function getOneSignalConfig() {
|
|
620
|
+
const appId = process.env.ONESIGNAL_APP_ID;
|
|
621
|
+
const apiKey = process.env.ONESIGNAL_API_KEY;
|
|
622
|
+
if (!appId || !apiKey) {
|
|
623
|
+
const config = await loadGlobalConfig();
|
|
624
|
+
return {
|
|
625
|
+
appId: appId || config.onesignal?.appId,
|
|
626
|
+
apiKey: apiKey || config.onesignal?.apiKey
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return { appId, apiKey };
|
|
630
|
+
}
|
|
631
|
+
async function sendNotification(target, message, data, scheduleAt) {
|
|
632
|
+
const { appId, apiKey } = await getOneSignalConfig();
|
|
633
|
+
if (!appId || !apiKey) {
|
|
634
|
+
throw new AuthError(
|
|
635
|
+
"OneSignal credentials not found",
|
|
636
|
+
"Set ONESIGNAL_APP_ID and ONESIGNAL_API_KEY environment variables"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const payload = {
|
|
640
|
+
app_id: appId,
|
|
641
|
+
contents: { en: message }
|
|
642
|
+
};
|
|
643
|
+
if (target.userId) {
|
|
644
|
+
payload.include_external_user_ids = [target.userId];
|
|
645
|
+
} else if (target.segment) {
|
|
646
|
+
payload.included_segments = [target.segment];
|
|
647
|
+
}
|
|
648
|
+
if (data) {
|
|
649
|
+
payload.data = data;
|
|
650
|
+
}
|
|
651
|
+
if (scheduleAt) {
|
|
652
|
+
payload.send_after = scheduleAt;
|
|
653
|
+
}
|
|
654
|
+
const response = await got2.post(`${ONESIGNAL_API}/notifications`, {
|
|
655
|
+
headers: {
|
|
656
|
+
Authorization: `Basic ${apiKey}`,
|
|
657
|
+
"Content-Type": "application/json"
|
|
658
|
+
},
|
|
659
|
+
json: payload
|
|
660
|
+
}).json();
|
|
661
|
+
return response;
|
|
662
|
+
}
|
|
663
|
+
var pushCommand = new Command7("push").description("OneSignal push notification management").addCommand(
|
|
664
|
+
new Command7("send").description("Send a push notification").argument("<target>", "User ID or segment name").argument("<message>", "Notification message").option("-s, --segment", "Target is a segment name").option("-d, --data <json>", "Additional data (JSON)").action(async (target, message, options) => {
|
|
665
|
+
const spinner = ora5("Sending notification...").start();
|
|
666
|
+
try {
|
|
667
|
+
const data = options.data ? JSON.parse(options.data) : void 0;
|
|
668
|
+
const targetObj = options.segment ? { segment: target } : { userId: target };
|
|
669
|
+
const result = await sendNotification(targetObj, message, data);
|
|
670
|
+
spinner.succeed("Notification sent");
|
|
671
|
+
console.log(pc4.green(`ID: ${result.id}`));
|
|
672
|
+
console.log(pc4.dim(`Recipients: ${result.recipients}`));
|
|
673
|
+
} catch (error) {
|
|
674
|
+
spinner.fail("Failed to send notification");
|
|
675
|
+
handleError(error);
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
).addCommand(
|
|
679
|
+
new Command7("schedule").description("Schedule a push notification").argument("<target>", "User ID").argument("<message>", "Notification message").option(
|
|
680
|
+
"--at <datetime>",
|
|
681
|
+
"Send time (ISO 8601 format)",
|
|
682
|
+
new Date(Date.now() + 36e5).toISOString()
|
|
683
|
+
).option("-d, --data <json>", "Additional data (JSON)").action(async (target, message, options) => {
|
|
684
|
+
const spinner = ora5("Scheduling notification...").start();
|
|
685
|
+
try {
|
|
686
|
+
const data = options.data ? JSON.parse(options.data) : void 0;
|
|
687
|
+
const result = await sendNotification({ userId: target }, message, data, options.at);
|
|
688
|
+
spinner.succeed("Notification scheduled");
|
|
689
|
+
console.log(pc4.green(`ID: ${result.id}`));
|
|
690
|
+
console.log(pc4.dim(`Scheduled for: ${options.at}`));
|
|
691
|
+
} catch (error) {
|
|
692
|
+
spinner.fail("Failed to schedule notification");
|
|
693
|
+
handleError(error);
|
|
694
|
+
}
|
|
695
|
+
})
|
|
696
|
+
).addCommand(
|
|
697
|
+
new Command7("template").description("Template operations").argument("<action>", "Action: list, send").argument("[templateId]", "Template ID (for send)").option("--to <userId>", "Target user ID (for send)").action(async (action, templateId, options) => {
|
|
698
|
+
const spinner = ora5(`Template ${action}...`).start();
|
|
699
|
+
try {
|
|
700
|
+
const { appId, apiKey } = await getOneSignalConfig();
|
|
701
|
+
if (!appId || !apiKey) {
|
|
702
|
+
throw new AuthError(
|
|
703
|
+
"OneSignal credentials not found",
|
|
704
|
+
"Set ONESIGNAL_APP_ID and ONESIGNAL_API_KEY environment variables"
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (action === "list") {
|
|
708
|
+
const response = await got2.get(`${ONESIGNAL_API}/templates?app_id=${appId}`, {
|
|
709
|
+
headers: { Authorization: `Basic ${apiKey}` }
|
|
710
|
+
}).json();
|
|
711
|
+
spinner.stop();
|
|
712
|
+
console.log(pc4.cyan("Templates:"));
|
|
713
|
+
for (const template of response.templates) {
|
|
714
|
+
console.log(` ${pc4.green(template.id)} - ${template.name}`);
|
|
715
|
+
}
|
|
716
|
+
} else if (action === "send") {
|
|
717
|
+
if (!templateId || !options.to) {
|
|
718
|
+
throw new Error("Template ID and target user required");
|
|
719
|
+
}
|
|
720
|
+
const response = await got2.post(`${ONESIGNAL_API}/notifications`, {
|
|
721
|
+
headers: {
|
|
722
|
+
Authorization: `Basic ${apiKey}`,
|
|
723
|
+
"Content-Type": "application/json"
|
|
724
|
+
},
|
|
725
|
+
json: {
|
|
726
|
+
app_id: appId,
|
|
727
|
+
template_id: templateId,
|
|
728
|
+
include_external_user_ids: [options.to]
|
|
729
|
+
}
|
|
730
|
+
}).json();
|
|
731
|
+
spinner.succeed("Template notification sent");
|
|
732
|
+
console.log(pc4.green(`ID: ${response.id}`));
|
|
733
|
+
}
|
|
734
|
+
} catch (error) {
|
|
735
|
+
spinner.fail(`Template ${action} failed`);
|
|
736
|
+
handleError(error);
|
|
737
|
+
}
|
|
738
|
+
})
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// src/commands/redis/index.ts
|
|
742
|
+
import { Command as Command8 } from "commander";
|
|
743
|
+
import ora6 from "ora";
|
|
744
|
+
import pc5 from "picocolors";
|
|
745
|
+
async function getRedisUrl() {
|
|
746
|
+
const url = process.env.REDIS_URL;
|
|
747
|
+
if (!url) {
|
|
748
|
+
const config = await loadGlobalConfig();
|
|
749
|
+
if (config.redis?.url) {
|
|
750
|
+
return config.redis.url;
|
|
751
|
+
}
|
|
752
|
+
throw new AuthError(
|
|
753
|
+
"REDIS_URL environment variable is not set",
|
|
754
|
+
"Set REDIS_URL to your Redis/DragonflyDB connection string"
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return url;
|
|
758
|
+
}
|
|
759
|
+
var redisCommand = new Command8("redis").description("Redis/DragonflyDB cache and queue management").addCommand(
|
|
760
|
+
new Command8("ping").description("Test Redis connection").action(async () => {
|
|
761
|
+
const spinner = ora6("Pinging Redis...").start();
|
|
762
|
+
try {
|
|
763
|
+
const url = await getRedisUrl();
|
|
764
|
+
spinner.succeed(`Connected to Redis at ${url.replace(/:[^:]+@/, ":****@")}`);
|
|
765
|
+
console.log(pc5.green("PONG"));
|
|
766
|
+
} catch (error) {
|
|
767
|
+
spinner.fail("Failed to connect to Redis");
|
|
768
|
+
handleError(error);
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
).addCommand(
|
|
772
|
+
new Command8("info").description("Get Redis server info").action(async () => {
|
|
773
|
+
const spinner = ora6("Fetching Redis info...").start();
|
|
774
|
+
try {
|
|
775
|
+
await getRedisUrl();
|
|
776
|
+
spinner.stop();
|
|
777
|
+
console.log(pc5.yellow("Note: Full Redis info requires ioredis integration"));
|
|
778
|
+
console.log("Install ioredis for full functionality: pnpm add ioredis");
|
|
779
|
+
} catch (error) {
|
|
780
|
+
spinner.fail("Failed to get Redis info");
|
|
781
|
+
handleError(error);
|
|
782
|
+
}
|
|
783
|
+
})
|
|
784
|
+
).addCommand(
|
|
785
|
+
new Command8("keys").description("List keys matching pattern").argument("<pattern>", "Key pattern (e.g., user:*)").action(async (pattern) => {
|
|
786
|
+
const spinner = ora6(`Searching keys: ${pattern}...`).start();
|
|
787
|
+
try {
|
|
788
|
+
await getRedisUrl();
|
|
789
|
+
spinner.stop();
|
|
790
|
+
console.log(pc5.yellow("Note: Key listing requires ioredis integration"));
|
|
791
|
+
} catch (error) {
|
|
792
|
+
spinner.fail("Failed to list keys");
|
|
793
|
+
handleError(error);
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
).addCommand(
|
|
797
|
+
new Command8("get").description("Get value by key").argument("<key>", "Key name").action(async (_key) => {
|
|
798
|
+
try {
|
|
799
|
+
await getRedisUrl();
|
|
800
|
+
console.log(pc5.yellow("Note: Get operation requires ioredis integration"));
|
|
801
|
+
} catch (error) {
|
|
802
|
+
handleError(error);
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
).addCommand(
|
|
806
|
+
new Command8("set").description("Set a key-value pair").argument("<key>", "Key name").argument("<value>", "Value").option("--ttl <seconds>", "Time to live in seconds").action(async (_key, _value, _options) => {
|
|
807
|
+
try {
|
|
808
|
+
await getRedisUrl();
|
|
809
|
+
console.log(pc5.yellow("Note: Set operation requires ioredis integration"));
|
|
810
|
+
} catch (error) {
|
|
811
|
+
handleError(error);
|
|
812
|
+
}
|
|
813
|
+
})
|
|
814
|
+
).addCommand(
|
|
815
|
+
new Command8("del").description("Delete a key").argument("<key>", "Key name").action(async (_key) => {
|
|
816
|
+
try {
|
|
817
|
+
await getRedisUrl();
|
|
818
|
+
console.log(pc5.yellow("Note: Delete operation requires ioredis integration"));
|
|
819
|
+
} catch (error) {
|
|
820
|
+
handleError(error);
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
).addCommand(
|
|
824
|
+
new Command8("queue").description("BullMQ queue operations").argument("<name>", "Queue name").argument(
|
|
825
|
+
"[action]",
|
|
826
|
+
"Action: status, waiting, active, failed, delayed, pause, resume, clean"
|
|
827
|
+
).option("--status <status>", "Job status for clean action").option("--age <hours>", "Job age in hours for clean action").action(async (name, action = "status", _options) => {
|
|
828
|
+
const spinner = ora6(`Queue ${name}: ${action}...`).start();
|
|
829
|
+
try {
|
|
830
|
+
await getRedisUrl();
|
|
831
|
+
spinner.stop();
|
|
832
|
+
if (action === "status") {
|
|
833
|
+
console.log(pc5.cyan(`Queue: ${name}`));
|
|
834
|
+
console.log(
|
|
835
|
+
formatTable(
|
|
836
|
+
["Status", "Count"],
|
|
837
|
+
[
|
|
838
|
+
["waiting", "0"],
|
|
839
|
+
["active", "0"],
|
|
840
|
+
["completed", "0"],
|
|
841
|
+
["failed", "0"],
|
|
842
|
+
["delayed", "0"]
|
|
843
|
+
]
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
console.log(pc5.yellow("\nNote: Full BullMQ integration requires bullmq package"));
|
|
848
|
+
} catch (error) {
|
|
849
|
+
spinner.fail(`Queue operation failed`);
|
|
850
|
+
handleError(error);
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
// src/commands/supabase/index.ts
|
|
856
|
+
import { Command as Command9 } from "commander";
|
|
857
|
+
import ora7 from "ora";
|
|
858
|
+
import pc6 from "picocolors";
|
|
859
|
+
|
|
860
|
+
// src/services/supabase.ts
|
|
861
|
+
import { exec as exec2 } from "child_process";
|
|
862
|
+
import { promisify as promisify2 } from "util";
|
|
863
|
+
var execAsync2 = promisify2(exec2);
|
|
864
|
+
async function checkSupabaseCLI() {
|
|
865
|
+
try {
|
|
866
|
+
await execAsync2("supabase --version");
|
|
867
|
+
return true;
|
|
868
|
+
} catch {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
async function runSupabaseCommand(args) {
|
|
873
|
+
const isInstalled = await checkSupabaseCLI();
|
|
874
|
+
if (!isInstalled) {
|
|
875
|
+
throw new CLIError(
|
|
876
|
+
"Supabase CLI not found",
|
|
877
|
+
1,
|
|
878
|
+
"Install with: brew install supabase/tap/supabase"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
const { stdout, stderr } = await execAsync2(`supabase ${args.join(" ")}`);
|
|
883
|
+
if (stderr && !stderr.includes("warning")) {
|
|
884
|
+
console.error(stderr);
|
|
885
|
+
}
|
|
886
|
+
return stdout;
|
|
887
|
+
} catch (error) {
|
|
888
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
889
|
+
throw new CLIError(`Supabase command failed: ${message}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
var RLS_POLICIES = {
|
|
893
|
+
"user-owned": (table, column) => `
|
|
894
|
+
-- RLS Policies for ${table} (user-owned)
|
|
895
|
+
|
|
896
|
+
-- Enable RLS
|
|
897
|
+
ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
|
|
898
|
+
|
|
899
|
+
-- Users can view their own records
|
|
900
|
+
CREATE POLICY "Users can view own ${table}"
|
|
901
|
+
ON ${table} FOR SELECT
|
|
902
|
+
USING (auth.uid() = ${column});
|
|
903
|
+
|
|
904
|
+
-- Users can create their own records
|
|
905
|
+
CREATE POLICY "Users can create own ${table}"
|
|
906
|
+
ON ${table} FOR INSERT
|
|
907
|
+
WITH CHECK (auth.uid() = ${column});
|
|
908
|
+
|
|
909
|
+
-- Users can update their own records
|
|
910
|
+
CREATE POLICY "Users can update own ${table}"
|
|
911
|
+
ON ${table} FOR UPDATE
|
|
912
|
+
USING (auth.uid() = ${column});
|
|
913
|
+
|
|
914
|
+
-- Users can delete their own records
|
|
915
|
+
CREATE POLICY "Users can delete own ${table}"
|
|
916
|
+
ON ${table} FOR DELETE
|
|
917
|
+
USING (auth.uid() = ${column});
|
|
918
|
+
`,
|
|
919
|
+
"team-owned": (table, teamColumn) => `
|
|
920
|
+
-- RLS Policies for ${table} (team-owned)
|
|
921
|
+
|
|
922
|
+
-- Enable RLS
|
|
923
|
+
ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
|
|
924
|
+
|
|
925
|
+
-- Team members can view team records
|
|
926
|
+
CREATE POLICY "Team members can view ${table}"
|
|
927
|
+
ON ${table} FOR SELECT
|
|
928
|
+
USING (
|
|
929
|
+
${teamColumn} IN (
|
|
930
|
+
SELECT team_id FROM team_members WHERE user_id = auth.uid()
|
|
931
|
+
)
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
-- Team members can create team records
|
|
935
|
+
CREATE POLICY "Team members can create ${table}"
|
|
936
|
+
ON ${table} FOR INSERT
|
|
937
|
+
WITH CHECK (
|
|
938
|
+
${teamColumn} IN (
|
|
939
|
+
SELECT team_id FROM team_members WHERE user_id = auth.uid()
|
|
940
|
+
)
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
-- Team members can update team records
|
|
944
|
+
CREATE POLICY "Team members can update ${table}"
|
|
945
|
+
ON ${table} FOR UPDATE
|
|
946
|
+
USING (
|
|
947
|
+
${teamColumn} IN (
|
|
948
|
+
SELECT team_id FROM team_members WHERE user_id = auth.uid()
|
|
949
|
+
)
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
-- Team admins can delete team records
|
|
953
|
+
CREATE POLICY "Team admins can delete ${table}"
|
|
954
|
+
ON ${table} FOR DELETE
|
|
955
|
+
USING (
|
|
956
|
+
${teamColumn} IN (
|
|
957
|
+
SELECT team_id FROM team_members WHERE user_id = auth.uid() AND role = 'admin'
|
|
958
|
+
)
|
|
959
|
+
);
|
|
960
|
+
`,
|
|
961
|
+
"public-read": (table) => `
|
|
962
|
+
-- RLS Policies for ${table} (public-read)
|
|
963
|
+
|
|
964
|
+
-- Enable RLS
|
|
965
|
+
ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
|
|
966
|
+
|
|
967
|
+
-- Anyone can view records
|
|
968
|
+
CREATE POLICY "Anyone can view ${table}"
|
|
969
|
+
ON ${table} FOR SELECT
|
|
970
|
+
USING (true);
|
|
971
|
+
|
|
972
|
+
-- Only authenticated users can create records
|
|
973
|
+
CREATE POLICY "Authenticated users can create ${table}"
|
|
974
|
+
ON ${table} FOR INSERT
|
|
975
|
+
WITH CHECK (auth.role() = 'authenticated');
|
|
976
|
+
|
|
977
|
+
-- Only owners can update their records
|
|
978
|
+
CREATE POLICY "Owners can update ${table}"
|
|
979
|
+
ON ${table} FOR UPDATE
|
|
980
|
+
USING (auth.uid() = user_id);
|
|
981
|
+
|
|
982
|
+
-- Only owners can delete their records
|
|
983
|
+
CREATE POLICY "Owners can delete ${table}"
|
|
984
|
+
ON ${table} FOR DELETE
|
|
985
|
+
USING (auth.uid() = user_id);
|
|
986
|
+
`,
|
|
987
|
+
"admin-only": (table) => `
|
|
988
|
+
-- RLS Policies for ${table} (admin-only)
|
|
989
|
+
|
|
990
|
+
-- Enable RLS
|
|
991
|
+
ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
|
|
992
|
+
|
|
993
|
+
-- Only admins can view records
|
|
994
|
+
CREATE POLICY "Admins can view ${table}"
|
|
995
|
+
ON ${table} FOR SELECT
|
|
996
|
+
USING (
|
|
997
|
+
auth.uid() IN (SELECT user_id FROM admins)
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
-- Only admins can create records
|
|
1001
|
+
CREATE POLICY "Admins can create ${table}"
|
|
1002
|
+
ON ${table} FOR INSERT
|
|
1003
|
+
WITH CHECK (
|
|
1004
|
+
auth.uid() IN (SELECT user_id FROM admins)
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
-- Only admins can update records
|
|
1008
|
+
CREATE POLICY "Admins can update ${table}"
|
|
1009
|
+
ON ${table} FOR UPDATE
|
|
1010
|
+
USING (
|
|
1011
|
+
auth.uid() IN (SELECT user_id FROM admins)
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
-- Only admins can delete records
|
|
1015
|
+
CREATE POLICY "Admins can delete ${table}"
|
|
1016
|
+
ON ${table} FOR DELETE
|
|
1017
|
+
USING (
|
|
1018
|
+
auth.uid() IN (SELECT user_id FROM admins)
|
|
1019
|
+
);
|
|
1020
|
+
`
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// src/commands/supabase/index.ts
|
|
1024
|
+
var supabaseCommand = new Command9("supabase").description("Supabase database management").addCommand(
|
|
1025
|
+
new Command9("schema").description("Show database schema").argument("[table]", "Specific table name").option("--rls", "Include RLS policies").option("--relationships", "Show foreign key relationships").action(async (_table, _options) => {
|
|
1026
|
+
const spinner = ora7("Fetching schema...").start();
|
|
1027
|
+
try {
|
|
1028
|
+
const output = await runSupabaseCommand(["db", "dump", "--schema", "public", "-f", "-"]);
|
|
1029
|
+
spinner.stop();
|
|
1030
|
+
console.log(output);
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
spinner.fail("Failed to fetch schema");
|
|
1033
|
+
handleError(error);
|
|
1034
|
+
}
|
|
1035
|
+
})
|
|
1036
|
+
).addCommand(
|
|
1037
|
+
new Command9("create-table").description("Generate CREATE TABLE SQL").argument("<name>", "Table name").option("-c, --columns <spec>", "Column specification").option("--from-freezed <file>", "Generate from Freezed model file").action(async (name, options) => {
|
|
1038
|
+
try {
|
|
1039
|
+
if (!options.columns) {
|
|
1040
|
+
throw new CLIError(
|
|
1041
|
+
"Columns specification required",
|
|
1042
|
+
1,
|
|
1043
|
+
'Use --columns "id:uuid:pk,title:text,user_id:uuid:fk(auth.users)"'
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const columns = parseColumnSpec(options.columns);
|
|
1047
|
+
const sql = columnsToSQL(name, columns);
|
|
1048
|
+
console.log(pc6.cyan("-- Migration: create_" + name + "_table"));
|
|
1049
|
+
console.log(sql);
|
|
1050
|
+
console.log("\n-- Enable RLS");
|
|
1051
|
+
console.log(`ALTER TABLE ${name} ENABLE ROW LEVEL SECURITY;`);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
handleError(error);
|
|
1054
|
+
}
|
|
1055
|
+
})
|
|
1056
|
+
).addCommand(
|
|
1057
|
+
new Command9("rls").description("Generate RLS policies").argument("<table>", "Table name").option(
|
|
1058
|
+
"-p, --policy <type>",
|
|
1059
|
+
"Policy type: user-owned, team-owned, public-read, admin-only",
|
|
1060
|
+
"user-owned"
|
|
1061
|
+
).option("-c, --column <name>", "User/team ID column", "user_id").action(async (table, options) => {
|
|
1062
|
+
try {
|
|
1063
|
+
const policyType = options.policy;
|
|
1064
|
+
if (!RLS_POLICIES[policyType]) {
|
|
1065
|
+
throw new CLIError(
|
|
1066
|
+
`Invalid policy type: "${policyType}"`,
|
|
1067
|
+
1,
|
|
1068
|
+
"Valid types: user-owned, team-owned, public-read, admin-only"
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
const sql = RLS_POLICIES[policyType](table, options.column);
|
|
1072
|
+
console.log(sql);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
handleError(error);
|
|
1075
|
+
}
|
|
1076
|
+
})
|
|
1077
|
+
).addCommand(
|
|
1078
|
+
new Command9("migration").description("Migration management").argument("<action>", "Action: new, apply, status").argument("[name]", "Migration name (for new)").action(async (action, name) => {
|
|
1079
|
+
const spinner = ora7(`Running migration ${action}...`).start();
|
|
1080
|
+
try {
|
|
1081
|
+
let args;
|
|
1082
|
+
switch (action) {
|
|
1083
|
+
case "new":
|
|
1084
|
+
if (!name) {
|
|
1085
|
+
throw new CLIError('Migration name required for "new" action');
|
|
1086
|
+
}
|
|
1087
|
+
args = ["migration", "new", name];
|
|
1088
|
+
break;
|
|
1089
|
+
case "apply":
|
|
1090
|
+
args = ["db", "push"];
|
|
1091
|
+
break;
|
|
1092
|
+
case "status":
|
|
1093
|
+
args = ["migration", "list"];
|
|
1094
|
+
break;
|
|
1095
|
+
default:
|
|
1096
|
+
throw new CLIError(
|
|
1097
|
+
`Invalid action: "${action}"`,
|
|
1098
|
+
1,
|
|
1099
|
+
"Valid actions: new, apply, status"
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
const output = await runSupabaseCommand(args);
|
|
1103
|
+
spinner.stop();
|
|
1104
|
+
console.log(output);
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
spinner.fail(`Migration ${action} failed`);
|
|
1107
|
+
handleError(error);
|
|
1108
|
+
}
|
|
1109
|
+
})
|
|
1110
|
+
).addCommand(
|
|
1111
|
+
new Command9("types").description("Generate TypeScript/Dart types").argument("<lang>", "Language: dart, typescript").option("-o, --output <path>", "Output file path").action(async (lang, options) => {
|
|
1112
|
+
const spinner = ora7(`Generating ${lang} types...`).start();
|
|
1113
|
+
try {
|
|
1114
|
+
let args;
|
|
1115
|
+
switch (lang) {
|
|
1116
|
+
case "dart":
|
|
1117
|
+
case "typescript":
|
|
1118
|
+
args = ["gen", "types", lang, "--local"];
|
|
1119
|
+
break;
|
|
1120
|
+
default:
|
|
1121
|
+
throw new CLIError(
|
|
1122
|
+
`Invalid language: "${lang}"`,
|
|
1123
|
+
1,
|
|
1124
|
+
"Valid languages: dart, typescript"
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
const output = await runSupabaseCommand(args);
|
|
1128
|
+
spinner.stop();
|
|
1129
|
+
if (options.output) {
|
|
1130
|
+
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
1131
|
+
await writeFile2(options.output, output);
|
|
1132
|
+
console.log(pc6.green(`\u2713 Generated ${options.output}`));
|
|
1133
|
+
} else {
|
|
1134
|
+
console.log(output);
|
|
1135
|
+
}
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
spinner.fail("Type generation failed");
|
|
1138
|
+
handleError(error);
|
|
1139
|
+
}
|
|
1140
|
+
})
|
|
1141
|
+
).addCommand(
|
|
1142
|
+
new Command9("fn").description("Edge function management").argument("<action>", "Action: new, deploy, logs").argument("<name>", "Function name").action(async (action, name) => {
|
|
1143
|
+
const spinner = ora7(`Running fn ${action}...`).start();
|
|
1144
|
+
try {
|
|
1145
|
+
let args;
|
|
1146
|
+
switch (action) {
|
|
1147
|
+
case "new":
|
|
1148
|
+
args = ["functions", "new", name];
|
|
1149
|
+
break;
|
|
1150
|
+
case "deploy":
|
|
1151
|
+
args = ["functions", "deploy", name];
|
|
1152
|
+
break;
|
|
1153
|
+
case "logs":
|
|
1154
|
+
args = ["functions", "logs", name];
|
|
1155
|
+
break;
|
|
1156
|
+
default:
|
|
1157
|
+
throw new CLIError(
|
|
1158
|
+
`Invalid action: "${action}"`,
|
|
1159
|
+
1,
|
|
1160
|
+
"Valid actions: new, deploy, logs"
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
const output = await runSupabaseCommand(args);
|
|
1164
|
+
spinner.stop();
|
|
1165
|
+
console.log(output);
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
spinner.fail(`Function ${action} failed`);
|
|
1168
|
+
handleError(error);
|
|
1169
|
+
}
|
|
1170
|
+
})
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
// src/commands/video/index.ts
|
|
1174
|
+
import { exec as exec3 } from "child_process";
|
|
1175
|
+
import { promisify as promisify3 } from "util";
|
|
1176
|
+
import { Command as Command10 } from "commander";
|
|
1177
|
+
import ora8 from "ora";
|
|
1178
|
+
import pc7 from "picocolors";
|
|
1179
|
+
var execAsync3 = promisify3(exec3);
|
|
1180
|
+
async function checkFFmpeg() {
|
|
1181
|
+
try {
|
|
1182
|
+
await execAsync3("ffmpeg -version");
|
|
1183
|
+
return true;
|
|
1184
|
+
} catch {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async function runFFmpeg(args) {
|
|
1189
|
+
const isInstalled = await checkFFmpeg();
|
|
1190
|
+
if (!isInstalled) {
|
|
1191
|
+
throw new CLIError(
|
|
1192
|
+
"FFmpeg not found",
|
|
1193
|
+
1,
|
|
1194
|
+
"Install FFmpeg: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
try {
|
|
1198
|
+
const { stdout, stderr } = await execAsync3(`ffmpeg ${args.join(" ")}`);
|
|
1199
|
+
return stdout || stderr;
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1202
|
+
throw new CLIError(`FFmpeg command failed: ${message}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
async function runFFprobe(args) {
|
|
1206
|
+
const isInstalled = await checkFFmpeg();
|
|
1207
|
+
if (!isInstalled) {
|
|
1208
|
+
throw new CLIError(
|
|
1209
|
+
"FFmpeg/FFprobe not found",
|
|
1210
|
+
1,
|
|
1211
|
+
"Install FFmpeg: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
const { stdout } = await execAsync3(`ffprobe ${args.join(" ")}`);
|
|
1216
|
+
return stdout;
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
const err = error;
|
|
1219
|
+
return err.stderr || "";
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
var videoCommand = new Command10("video").description("Video processing with FFmpeg").addCommand(
|
|
1223
|
+
new Command10("info").description("Get video file information").argument("<file>", "Video file path").action(async (file) => {
|
|
1224
|
+
const spinner = ora8("Analyzing video...").start();
|
|
1225
|
+
try {
|
|
1226
|
+
const output = await runFFprobe([
|
|
1227
|
+
"-v",
|
|
1228
|
+
"quiet",
|
|
1229
|
+
"-print_format",
|
|
1230
|
+
"json",
|
|
1231
|
+
"-show_format",
|
|
1232
|
+
"-show_streams",
|
|
1233
|
+
`"${file}"`
|
|
1234
|
+
]);
|
|
1235
|
+
spinner.stop();
|
|
1236
|
+
try {
|
|
1237
|
+
const info = JSON.parse(output);
|
|
1238
|
+
const format = info.format || {};
|
|
1239
|
+
const videoStream = info.streams?.find((s) => s.codec_type === "video") || {};
|
|
1240
|
+
const audioStream = info.streams?.find((s) => s.codec_type === "audio") || {};
|
|
1241
|
+
console.log(pc7.cyan("Video Information:"));
|
|
1242
|
+
console.log(` File: ${format.filename || file}`);
|
|
1243
|
+
console.log(
|
|
1244
|
+
` Duration: ${format.duration ? Number.parseFloat(format.duration).toFixed(2) + "s" : "N/A"}`
|
|
1245
|
+
);
|
|
1246
|
+
console.log(
|
|
1247
|
+
` Size: ${format.size ? (Number.parseInt(format.size, 10) / 1024 / 1024).toFixed(2) + " MB" : "N/A"}`
|
|
1248
|
+
);
|
|
1249
|
+
console.log(
|
|
1250
|
+
` Bitrate: ${format.bit_rate ? (Number.parseInt(format.bit_rate, 10) / 1e3).toFixed(0) + " kbps" : "N/A"}`
|
|
1251
|
+
);
|
|
1252
|
+
if (videoStream.width) {
|
|
1253
|
+
console.log(pc7.cyan("\nVideo Stream:"));
|
|
1254
|
+
console.log(` Resolution: ${videoStream.width}x${videoStream.height}`);
|
|
1255
|
+
console.log(` Codec: ${videoStream.codec_name || "N/A"}`);
|
|
1256
|
+
console.log(` FPS: ${videoStream.r_frame_rate || "N/A"}`);
|
|
1257
|
+
}
|
|
1258
|
+
if (audioStream.codec_name) {
|
|
1259
|
+
console.log(pc7.cyan("\nAudio Stream:"));
|
|
1260
|
+
console.log(` Codec: ${audioStream.codec_name}`);
|
|
1261
|
+
console.log(` Sample Rate: ${audioStream.sample_rate || "N/A"} Hz`);
|
|
1262
|
+
console.log(` Channels: ${audioStream.channels || "N/A"}`);
|
|
1263
|
+
}
|
|
1264
|
+
} catch {
|
|
1265
|
+
console.log(output);
|
|
1266
|
+
}
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
spinner.fail("Failed to analyze video");
|
|
1269
|
+
handleError(error);
|
|
1270
|
+
}
|
|
1271
|
+
})
|
|
1272
|
+
).addCommand(
|
|
1273
|
+
new Command10("combine").description("Combine video and audio").argument("<video>", "Video file path").argument("<audio>", "Audio file path").requiredOption("-o, --output <file>", "Output file path").action(async (video, audio, options) => {
|
|
1274
|
+
const spinner = ora8("Combining video and audio...").start();
|
|
1275
|
+
try {
|
|
1276
|
+
await runFFmpeg([
|
|
1277
|
+
"-i",
|
|
1278
|
+
`"${video}"`,
|
|
1279
|
+
"-i",
|
|
1280
|
+
`"${audio}"`,
|
|
1281
|
+
"-c:v",
|
|
1282
|
+
"copy",
|
|
1283
|
+
"-c:a",
|
|
1284
|
+
"aac",
|
|
1285
|
+
"-y",
|
|
1286
|
+
`"${options.output}"`
|
|
1287
|
+
]);
|
|
1288
|
+
spinner.succeed(`Combined: ${options.output}`);
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
spinner.fail("Failed to combine video and audio");
|
|
1291
|
+
handleError(error);
|
|
1292
|
+
}
|
|
1293
|
+
})
|
|
1294
|
+
).addCommand(
|
|
1295
|
+
new Command10("thumbnail").description("Extract thumbnail from video").argument("<video>", "Video file path").option("--at <timestamp>", "Timestamp (e.g., 00:00:05)", "00:00:01").requiredOption("-o, --output <file>", "Output file path").action(async (video, options) => {
|
|
1296
|
+
const spinner = ora8("Extracting thumbnail...").start();
|
|
1297
|
+
try {
|
|
1298
|
+
await runFFmpeg([
|
|
1299
|
+
"-i",
|
|
1300
|
+
`"${video}"`,
|
|
1301
|
+
"-ss",
|
|
1302
|
+
options.at,
|
|
1303
|
+
"-vframes",
|
|
1304
|
+
"1",
|
|
1305
|
+
"-y",
|
|
1306
|
+
`"${options.output}"`
|
|
1307
|
+
]);
|
|
1308
|
+
spinner.succeed(`Thumbnail: ${options.output}`);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
spinner.fail("Failed to extract thumbnail");
|
|
1311
|
+
handleError(error);
|
|
1312
|
+
}
|
|
1313
|
+
})
|
|
1314
|
+
).addCommand(
|
|
1315
|
+
new Command10("resize").description("Resize video").argument("<video>", "Video file path").requiredOption("-s, --size <WxH>", "New size (e.g., 1280x720)").requiredOption("-o, --output <file>", "Output file path").action(async (video, options) => {
|
|
1316
|
+
const spinner = ora8("Resizing video...").start();
|
|
1317
|
+
try {
|
|
1318
|
+
await runFFmpeg([
|
|
1319
|
+
"-i",
|
|
1320
|
+
`"${video}"`,
|
|
1321
|
+
"-vf",
|
|
1322
|
+
`scale=${options.size}`,
|
|
1323
|
+
"-y",
|
|
1324
|
+
`"${options.output}"`
|
|
1325
|
+
]);
|
|
1326
|
+
spinner.succeed(`Resized: ${options.output}`);
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
spinner.fail("Failed to resize video");
|
|
1329
|
+
handleError(error);
|
|
1330
|
+
}
|
|
1331
|
+
})
|
|
1332
|
+
).addCommand(
|
|
1333
|
+
new Command10("compress").description("Compress video").argument("<video>", "Video file path").option("-q, --quality <crf>", "Quality (1-51, lower is better)", "23").requiredOption("-o, --output <file>", "Output file path").action(async (video, options) => {
|
|
1334
|
+
const spinner = ora8("Compressing video...").start();
|
|
1335
|
+
try {
|
|
1336
|
+
await runFFmpeg([
|
|
1337
|
+
"-i",
|
|
1338
|
+
`"${video}"`,
|
|
1339
|
+
"-c:v",
|
|
1340
|
+
"libx264",
|
|
1341
|
+
"-crf",
|
|
1342
|
+
options.quality,
|
|
1343
|
+
"-preset",
|
|
1344
|
+
"medium",
|
|
1345
|
+
"-y",
|
|
1346
|
+
`"${options.output}"`
|
|
1347
|
+
]);
|
|
1348
|
+
spinner.succeed(`Compressed: ${options.output}`);
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
spinner.fail("Failed to compress video");
|
|
1351
|
+
handleError(error);
|
|
1352
|
+
}
|
|
1353
|
+
})
|
|
1354
|
+
).addCommand(
|
|
1355
|
+
new Command10("trim").description("Trim video").argument("<video>", "Video file path").requiredOption("--start <time>", "Start time (e.g., 00:00:10)").requiredOption("--end <time>", "End time (e.g., 00:01:00)").requiredOption("-o, --output <file>", "Output file path").action(async (video, options) => {
|
|
1356
|
+
const spinner = ora8("Trimming video...").start();
|
|
1357
|
+
try {
|
|
1358
|
+
await runFFmpeg([
|
|
1359
|
+
"-i",
|
|
1360
|
+
`"${video}"`,
|
|
1361
|
+
"-ss",
|
|
1362
|
+
options.start,
|
|
1363
|
+
"-to",
|
|
1364
|
+
options.end,
|
|
1365
|
+
"-c",
|
|
1366
|
+
"copy",
|
|
1367
|
+
"-y",
|
|
1368
|
+
`"${options.output}"`
|
|
1369
|
+
]);
|
|
1370
|
+
spinner.succeed(`Trimmed: ${options.output}`);
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
spinner.fail("Failed to trim video");
|
|
1373
|
+
handleError(error);
|
|
1374
|
+
}
|
|
1375
|
+
})
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
// src/cli.ts
|
|
1379
|
+
var VERSION = "1.0.0";
|
|
1380
|
+
function createProgram() {
|
|
1381
|
+
const program = new Command11();
|
|
1382
|
+
program.name("saas").description("A unified CLI for Flutter SaaS development").version(VERSION, "-V, --version", "Display version number").configureHelp({
|
|
1383
|
+
sortSubcommands: true,
|
|
1384
|
+
sortOptions: true
|
|
1385
|
+
}).option("--json", "Output results as JSON").option("-v, --verbose", "Enable verbose output").option("--debug", "Enable debug output").hook("preAction", (thisCommand) => {
|
|
1386
|
+
const opts = thisCommand.opts();
|
|
1387
|
+
process.env.SAAS_CLI_JSON = opts.json ? "1" : "";
|
|
1388
|
+
process.env.SAAS_CLI_VERBOSE = opts.verbose ? "1" : "";
|
|
1389
|
+
process.env.SAAS_CLI_DEBUG = opts.debug ? "1" : "";
|
|
1390
|
+
});
|
|
1391
|
+
program.addCommand(docsCommand);
|
|
1392
|
+
program.addCommand(askCommand);
|
|
1393
|
+
program.addCommand(genCommand);
|
|
1394
|
+
program.addCommand(supabaseCommand);
|
|
1395
|
+
program.addCommand(redisCommand);
|
|
1396
|
+
program.addCommand(cfCommand);
|
|
1397
|
+
program.addCommand(pushCommand);
|
|
1398
|
+
program.addCommand(flagsCommand);
|
|
1399
|
+
program.addCommand(videoCommand);
|
|
1400
|
+
program.addCommand(initCommand);
|
|
1401
|
+
program.addHelpText(
|
|
1402
|
+
"after",
|
|
1403
|
+
`
|
|
1404
|
+
${pc8.cyan("Examples:")}
|
|
1405
|
+
${pc8.dim("# Look up Flutter documentation")}
|
|
1406
|
+
$ saas docs flutter "ListView.builder with pagination"
|
|
1407
|
+
|
|
1408
|
+
${pc8.dim("# Ask AI a question")}
|
|
1409
|
+
$ saas ask "best practices for offline-first Flutter apps"
|
|
1410
|
+
|
|
1411
|
+
${pc8.dim("# Generate a Riverpod notifier")}
|
|
1412
|
+
$ saas gen riverpod notifier UserList --state "List<User>"
|
|
1413
|
+
|
|
1414
|
+
${pc8.dim("# Generate RLS policies")}
|
|
1415
|
+
$ saas supabase rls recipes --policy user-owned --column user_id
|
|
1416
|
+
|
|
1417
|
+
${pc8.dim("# Check feature flag status")}
|
|
1418
|
+
$ saas flags list
|
|
1419
|
+
|
|
1420
|
+
${pc8.cyan("Documentation:")}
|
|
1421
|
+
https://github.com/your-username/saas-cli
|
|
1422
|
+
`
|
|
1423
|
+
);
|
|
1424
|
+
return program;
|
|
1425
|
+
}
|
|
1426
|
+
async function run() {
|
|
1427
|
+
const program = createProgram();
|
|
1428
|
+
try {
|
|
1429
|
+
await program.parseAsync(process.argv);
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
handleError(error);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/index.ts
|
|
1436
|
+
run();
|
|
1437
|
+
//# sourceMappingURL=index.js.map
|