automate.ax 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/automation/actions.d.ts +200 -0
- package/dist/automation/actions.d.ts.map +1 -0
- package/dist/automation/actions.js +286 -0
- package/dist/automation/automation.d.ts +16 -0
- package/dist/automation/automation.d.ts.map +1 -0
- package/dist/automation/automation.js +10 -0
- package/dist/automation/integrations.d.ts +63 -0
- package/dist/automation/integrations.d.ts.map +1 -0
- package/dist/automation/integrations.js +58 -0
- package/dist/automation/runtime.d.ts +73 -0
- package/dist/automation/runtime.d.ts.map +1 -0
- package/dist/automation/runtime.js +92 -0
- package/dist/automation/signal.d.ts +58 -0
- package/dist/automation/signal.d.ts.map +1 -0
- package/dist/automation/signal.js +80 -0
- package/dist/automation/subscribe.d.ts +10 -0
- package/dist/automation/subscribe.d.ts.map +1 -0
- package/dist/automation/subscribe.js +16 -0
- package/dist/cli/command-names.d.ts +46 -0
- package/dist/cli/command-names.d.ts.map +1 -0
- package/dist/cli/command-names.js +46 -0
- package/dist/cli/commands/accept.d.ts +20 -0
- package/dist/cli/commands/accept.d.ts.map +1 -0
- package/dist/cli/commands/accept.js +42 -0
- package/dist/cli/commands/debug.d.ts +4 -0
- package/dist/cli/commands/debug.d.ts.map +1 -0
- package/dist/cli/commands/debug.js +52 -0
- package/dist/cli/commands/deploy.d.ts +24 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -0
- package/dist/cli/commands/deploy.js +292 -0
- package/dist/cli/commands/login.d.ts +20 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +145 -0
- package/dist/cli/commands/logout.d.ts +4 -0
- package/dist/cli/commands/logout.d.ts.map +1 -0
- package/dist/cli/commands/logout.js +32 -0
- package/dist/cli/commands/me.d.ts +4 -0
- package/dist/cli/commands/me.d.ts.map +1 -0
- package/dist/cli/commands/me.js +48 -0
- package/dist/cli/commands/org-api-key-create.d.ts +26 -0
- package/dist/cli/commands/org-api-key-create.d.ts.map +1 -0
- package/dist/cli/commands/org-api-key-create.js +51 -0
- package/dist/cli/commands/org-api-key-delete.d.ts +33 -0
- package/dist/cli/commands/org-api-key-delete.d.ts.map +1 -0
- package/dist/cli/commands/org-api-key-delete.js +57 -0
- package/dist/cli/commands/org-api-key-list.d.ts +16 -0
- package/dist/cli/commands/org-api-key-list.d.ts.map +1 -0
- package/dist/cli/commands/org-api-key-list.js +58 -0
- package/dist/cli/commands/org-api-key-rename.d.ts +32 -0
- package/dist/cli/commands/org-api-key-rename.d.ts.map +1 -0
- package/dist/cli/commands/org-api-key-rename.js +54 -0
- package/dist/cli/commands/org-api-key.d.ts +2 -0
- package/dist/cli/commands/org-api-key.d.ts.map +1 -0
- package/dist/cli/commands/org-api-key.js +18 -0
- package/dist/cli/commands/org-billing.d.ts +16 -0
- package/dist/cli/commands/org-billing.d.ts.map +1 -0
- package/dist/cli/commands/org-billing.js +78 -0
- package/dist/cli/commands/org-create.d.ts +20 -0
- package/dist/cli/commands/org-create.d.ts.map +1 -0
- package/dist/cli/commands/org-create.js +64 -0
- package/dist/cli/commands/org-delete.d.ts +27 -0
- package/dist/cli/commands/org-delete.d.ts.map +1 -0
- package/dist/cli/commands/org-delete.js +53 -0
- package/dist/cli/commands/org-invite.d.ts +39 -0
- package/dist/cli/commands/org-invite.d.ts.map +1 -0
- package/dist/cli/commands/org-invite.js +66 -0
- package/dist/cli/commands/org-leave.d.ts +27 -0
- package/dist/cli/commands/org-leave.d.ts.map +1 -0
- package/dist/cli/commands/org-leave.js +56 -0
- package/dist/cli/commands/org-list.d.ts +4 -0
- package/dist/cli/commands/org-list.d.ts.map +1 -0
- package/dist/cli/commands/org-list.js +36 -0
- package/dist/cli/commands/org-remove.d.ts +33 -0
- package/dist/cli/commands/org-remove.d.ts.map +1 -0
- package/dist/cli/commands/org-remove.js +66 -0
- package/dist/cli/commands/org-rename.d.ts +26 -0
- package/dist/cli/commands/org-rename.d.ts.map +1 -0
- package/dist/cli/commands/org-rename.js +58 -0
- package/dist/cli/commands/org-show.d.ts +16 -0
- package/dist/cli/commands/org-show.d.ts.map +1 -0
- package/dist/cli/commands/org-show.js +48 -0
- package/dist/cli/commands/org-upgrade.d.ts +42 -0
- package/dist/cli/commands/org-upgrade.d.ts.map +1 -0
- package/dist/cli/commands/org-upgrade.js +171 -0
- package/dist/cli/commands/org.d.ts +2 -0
- package/dist/cli/commands/org.d.ts.map +1 -0
- package/dist/cli/commands/org.js +32 -0
- package/dist/cli/commands/project-create.d.ts +27 -0
- package/dist/cli/commands/project-create.d.ts.map +1 -0
- package/dist/cli/commands/project-create.js +61 -0
- package/dist/cli/commands/project-delete.d.ts +27 -0
- package/dist/cli/commands/project-delete.d.ts.map +1 -0
- package/dist/cli/commands/project-delete.js +55 -0
- package/dist/cli/commands/project-list.d.ts +17 -0
- package/dist/cli/commands/project-list.d.ts.map +1 -0
- package/dist/cli/commands/project-list.js +89 -0
- package/dist/cli/commands/project-rename.d.ts +26 -0
- package/dist/cli/commands/project-rename.d.ts.map +1 -0
- package/dist/cli/commands/project-rename.js +60 -0
- package/dist/cli/commands/project-show.d.ts +16 -0
- package/dist/cli/commands/project-show.d.ts.map +1 -0
- package/dist/cli/commands/project-show.js +37 -0
- package/dist/cli/commands/project.d.ts +2 -0
- package/dist/cli/commands/project.d.ts.map +1 -0
- package/dist/cli/commands/project.js +20 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +63 -0
- package/dist/cli/lib/api-client.d.ts +3 -0
- package/dist/cli/lib/api-client.d.ts.map +1 -0
- package/dist/cli/lib/api-client.js +14 -0
- package/dist/cli/lib/auth-client.d.ts +80 -0
- package/dist/cli/lib/auth-client.d.ts.map +1 -0
- package/dist/cli/lib/auth-client.js +61 -0
- package/dist/cli/lib/automation-build.d.ts +30 -0
- package/dist/cli/lib/automation-build.d.ts.map +1 -0
- package/dist/cli/lib/automation-build.js +90 -0
- package/dist/cli/lib/config-dir.d.ts +47 -0
- package/dist/cli/lib/config-dir.d.ts.map +1 -0
- package/dist/cli/lib/config-dir.js +146 -0
- package/dist/cli/lib/detail-block.d.ts +16 -0
- package/dist/cli/lib/detail-block.d.ts.map +1 -0
- package/dist/cli/lib/detail-block.js +25 -0
- package/dist/cli/lib/env.d.ts +4 -0
- package/dist/cli/lib/env.d.ts.map +1 -0
- package/dist/cli/lib/env.js +7 -0
- package/dist/cli/lib/hijack-clack-logger.d.ts +2 -0
- package/dist/cli/lib/hijack-clack-logger.d.ts.map +1 -0
- package/dist/cli/lib/hijack-clack-logger.js +8 -0
- package/dist/cli/lib/io.d.ts +19 -0
- package/dist/cli/lib/io.d.ts.map +1 -0
- package/dist/cli/lib/io.js +87 -0
- package/dist/cli/lib/json-output.d.ts +9 -0
- package/dist/cli/lib/json-output.d.ts.map +1 -0
- package/dist/cli/lib/json-output.js +12 -0
- package/dist/cli/lib/organization-detail-block.d.ts +30 -0
- package/dist/cli/lib/organization-detail-block.d.ts.map +1 -0
- package/dist/cli/lib/organization-detail-block.js +72 -0
- package/dist/cli/lib/project-config.d.ts +10 -0
- package/dist/cli/lib/project-config.d.ts.map +1 -0
- package/dist/cli/lib/project-config.js +21 -0
- package/dist/cli/lib/spinner.d.ts +18 -0
- package/dist/cli/lib/spinner.d.ts.map +1 -0
- package/dist/cli/lib/spinner.js +111 -0
- package/dist/cli/usage.d.ts +9 -0
- package/dist/cli/usage.d.ts.map +1 -0
- package/dist/cli/usage.js +468 -0
- package/dist/cli/utils.d.ts +291 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +561 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +5 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +9 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/package.json +92 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { cancel, log as clackLog, confirm, intro, isCancel, isCI, log, select, text, } from "@clack/prompts";
|
|
2
|
+
import { bgYellow, red } from "ansis";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { z, ZodError } from "zod";
|
|
7
|
+
import { commandExamples } from "./command-names.js";
|
|
8
|
+
import { apiClient } from "./lib/api-client.js";
|
|
9
|
+
import { authClient } from "./lib/auth-client.js";
|
|
10
|
+
import { accent, dimmed } from "./lib/io.js";
|
|
11
|
+
import { isJsonOutput, writeJson } from "./lib/json-output.js";
|
|
12
|
+
import { findConfigDirMatches, findExplicitConfigDirMatch, } from "./lib/config-dir.js";
|
|
13
|
+
import { clearSpinner, startSpinner } from "./lib/spinner.js";
|
|
14
|
+
export const SESSION_PATH = path.join(os.homedir(), ".automate/session.json");
|
|
15
|
+
export const REASONABLE_NAME_SCHEMA = z.string().min(1).max(255).trim();
|
|
16
|
+
export const ORG_OPT = {
|
|
17
|
+
org: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Organization name or ID",
|
|
20
|
+
alias: ["o"],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export const API_KEY_OPT = {
|
|
24
|
+
key: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "API key name, prefix, or ID",
|
|
27
|
+
alias: ["k"],
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export const PROJECT_OPT = {
|
|
31
|
+
project: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Project name or ID",
|
|
34
|
+
alias: ["p"],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
export const CLI_PREFIX = bgYellow.black.bold ` automate `;
|
|
38
|
+
/** Reads and validates the saved CLI session token. */
|
|
39
|
+
export function loadSavedTokenSync() {
|
|
40
|
+
try {
|
|
41
|
+
return z
|
|
42
|
+
.object({ token: z.string() })
|
|
43
|
+
.parse(JSON.parse(fs.readFileSync(SESSION_PATH, "utf-8"))).token;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Persists the session token to disk for use by subsequent CLI commands.
|
|
51
|
+
*
|
|
52
|
+
* @param token - Bearer token to save.
|
|
53
|
+
*/
|
|
54
|
+
export function saveTokenSync(token) {
|
|
55
|
+
fs.mkdirSync(path.dirname(SESSION_PATH), { recursive: true });
|
|
56
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify({ token }, null, 2));
|
|
57
|
+
}
|
|
58
|
+
/** Removes the saved CLI session token. */
|
|
59
|
+
export function clearSavedTokenSync() {
|
|
60
|
+
fs.unlinkSync(SESSION_PATH);
|
|
61
|
+
}
|
|
62
|
+
/** Checks whether the current process can safely show interactive prompts. */
|
|
63
|
+
function canPrompt() {
|
|
64
|
+
return process.stdin.isTTY && !isCI() && !isJsonOutput();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Makes an option nullable only when the CLI can ask for it interactively.
|
|
68
|
+
*
|
|
69
|
+
* @param schema - Schema for the option's explicit value.
|
|
70
|
+
* @param optionName - Human-readable option name used in validation errors.
|
|
71
|
+
*/
|
|
72
|
+
export function promptableOption(schema, optionName) {
|
|
73
|
+
return schema
|
|
74
|
+
.nullable()
|
|
75
|
+
.prefault(null)
|
|
76
|
+
.refine((value) => value != null || canPrompt(), `${optionName ?? "Option"} must be passed explicitly in non-interactive mode.`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Runs an interactive prompt and handles cancellation consistently.
|
|
80
|
+
*
|
|
81
|
+
* @param promptFactory - Function that starts the prompt.
|
|
82
|
+
* @param optionName - Explicit option users can pass in non-interactive mode.
|
|
83
|
+
* @throws {Error} When prompting is unavailable.
|
|
84
|
+
*/
|
|
85
|
+
export async function safelyPrompt(promptFactory, optionName) {
|
|
86
|
+
if (!canPrompt()) {
|
|
87
|
+
throw new Error(`${optionName ?? "Option"} must be passed explicitly in non-interactive mode.`);
|
|
88
|
+
}
|
|
89
|
+
const result = await promptFactory();
|
|
90
|
+
if (isCancel(result)) {
|
|
91
|
+
clearSpinner();
|
|
92
|
+
cancel("Cancelled");
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Prompts for text, displays schema validation errors, and parses the result.
|
|
99
|
+
*
|
|
100
|
+
* @param promptOptions - Text prompt labels and defaults.
|
|
101
|
+
* @param schema - Schema used to validate and transform the response.
|
|
102
|
+
* @param optionName - Explicit option users can pass instead of prompting.
|
|
103
|
+
*/
|
|
104
|
+
export async function safelyPromptTextWithValidation(promptOptions, schema, optionName) {
|
|
105
|
+
return schema.parseAsync(await safelyPrompt(() => text({
|
|
106
|
+
validate: (v) => {
|
|
107
|
+
const validation = schema.safeParse(v);
|
|
108
|
+
return validation.success
|
|
109
|
+
? undefined
|
|
110
|
+
: z.prettifyError(validation.error);
|
|
111
|
+
},
|
|
112
|
+
...promptOptions,
|
|
113
|
+
}), optionName));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extracts the most specific message from common API error envelopes.
|
|
117
|
+
*
|
|
118
|
+
* @param error - Unknown error value returned by a command dependency.
|
|
119
|
+
*/
|
|
120
|
+
function extractErrorMessage(error) {
|
|
121
|
+
const errorWithMessage = z.object({ message: z.string() }).safeParse(error);
|
|
122
|
+
const bodyWithMessage = z
|
|
123
|
+
.looseObject({ body: z.looseObject({ message: z.string() }) })
|
|
124
|
+
.safeParse(error);
|
|
125
|
+
const causeWithMessage = z
|
|
126
|
+
.looseObject({ cause: z.looseObject({ message: z.string() }) })
|
|
127
|
+
.safeParse(error);
|
|
128
|
+
const causeBodyWithMessage = z
|
|
129
|
+
.looseObject({
|
|
130
|
+
cause: z.looseObject({
|
|
131
|
+
body: z.looseObject({ message: z.string() }),
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
.safeParse(error);
|
|
135
|
+
return errorWithMessage.success
|
|
136
|
+
? errorWithMessage.data.message
|
|
137
|
+
: bodyWithMessage.success
|
|
138
|
+
? bodyWithMessage.data.body.message
|
|
139
|
+
: causeWithMessage.success
|
|
140
|
+
? causeWithMessage.data.cause.message
|
|
141
|
+
: causeBodyWithMessage.success
|
|
142
|
+
? causeBodyWithMessage.data.cause.body.message
|
|
143
|
+
: String(error);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Formats an error for human-readable CLI output.
|
|
147
|
+
*
|
|
148
|
+
* @param error - Error to format.
|
|
149
|
+
*/
|
|
150
|
+
function formatErrorMessage(error) {
|
|
151
|
+
if (error instanceof ZodError)
|
|
152
|
+
return z.prettifyError(error);
|
|
153
|
+
return extractErrorMessage(error).replaceAll(/`([^`]+)`/g, (_, command) => accent(command));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Converts an error into the CLI's stable JSON error envelope.
|
|
157
|
+
*
|
|
158
|
+
* @param error - Error to serialize.
|
|
159
|
+
*/
|
|
160
|
+
export function formatJsonError(error) {
|
|
161
|
+
if (error instanceof ZodError) {
|
|
162
|
+
return {
|
|
163
|
+
error: {
|
|
164
|
+
message: "Failed validation",
|
|
165
|
+
...z.flattenError(error),
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
error: {
|
|
171
|
+
message: extractErrorMessage(error),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Writes a serialized CLI error to stderr.
|
|
177
|
+
*
|
|
178
|
+
* @param error - Error to serialize and write.
|
|
179
|
+
*/
|
|
180
|
+
export function writeJsonError(error) {
|
|
181
|
+
process.stderr.write(`${JSON.stringify(formatJsonError(error), null, 2)}\n`);
|
|
182
|
+
}
|
|
183
|
+
export const output = {
|
|
184
|
+
normal: (printFn) => {
|
|
185
|
+
if (!isJsonOutput())
|
|
186
|
+
printFn();
|
|
187
|
+
return output;
|
|
188
|
+
},
|
|
189
|
+
json: (value) => {
|
|
190
|
+
if (isJsonOutput())
|
|
191
|
+
writeJson(value);
|
|
192
|
+
return output;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
export async function useSession({ signInAnonymously = true, } = {}) {
|
|
196
|
+
return authClient.getSession().then(async (session) => {
|
|
197
|
+
if (session || !signInAnonymously)
|
|
198
|
+
return session;
|
|
199
|
+
return await authClient.signIn.anonymous();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Prints the CLI banner and current authentication identity.
|
|
204
|
+
*
|
|
205
|
+
* @param session - Session whose identity should be displayed.
|
|
206
|
+
* @param options - Intro display options.
|
|
207
|
+
* @param options.skipAnonWarning - Whether to suppress the anonymous-user
|
|
208
|
+
* warning.
|
|
209
|
+
*/
|
|
210
|
+
export function sessionIntro(session, { skipAnonWarning = false } = {}) {
|
|
211
|
+
intro(`${CLI_PREFIX} ${dimmed(session && !session.user.isAnonymous
|
|
212
|
+
? session.user.email
|
|
213
|
+
: "Not logged in")}`);
|
|
214
|
+
if (!skipAnonWarning && session?.user.isAnonymous) {
|
|
215
|
+
log.warn(dimmed.italic `You're using Automate.ax anonymously. Some features will be limited. Sign in with ${accent(commandExamples.login)} for full functionality.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Requires a real user session. Anonymous sessions can bootstrap local flows,
|
|
220
|
+
* but organization management needs an authenticated account.
|
|
221
|
+
*
|
|
222
|
+
* @param action - User-facing action that requires authentication.
|
|
223
|
+
* @throws {Error} When the current user is signed out or anonymous.
|
|
224
|
+
*/
|
|
225
|
+
export async function requireAuthenticatedSession(action) {
|
|
226
|
+
const session = await useSession({ signInAnonymously: false });
|
|
227
|
+
if (!session || session.user.isAnonymous) {
|
|
228
|
+
output.normal(() => sessionIntro(session, { skipAnonWarning: true }));
|
|
229
|
+
throw new Error(`You must log in with ${accent(commandExamples.login)} to ${action}.`);
|
|
230
|
+
}
|
|
231
|
+
return session;
|
|
232
|
+
}
|
|
233
|
+
/** Shared schema fragment for commands that operate on an organization. */
|
|
234
|
+
export const orgOptsSchema = z.object({
|
|
235
|
+
org: z.string().optional(),
|
|
236
|
+
});
|
|
237
|
+
/** Shared schema fragment for commands that select an organization API key. */
|
|
238
|
+
export const apiKeyOptsSchema = orgOptsSchema.extend({
|
|
239
|
+
key: z.string().optional(),
|
|
240
|
+
});
|
|
241
|
+
/**
|
|
242
|
+
* Resolves an organization from CLI flags or interactive prompt. Precedence:
|
|
243
|
+
* `--org` flag > auto-select (single org) > prompt. Callers opt into inline
|
|
244
|
+
* creation so destructive/update flows don't create empty organizations.
|
|
245
|
+
*
|
|
246
|
+
* @param opts - Parsed organization CLI options.
|
|
247
|
+
* @param options - Empty-organization behavior options.
|
|
248
|
+
* @param options.whenEmpty - Behavior when the user belongs to no
|
|
249
|
+
* organizations.
|
|
250
|
+
*/
|
|
251
|
+
export async function useOrganization(opts, { whenEmpty = "error" } = {}) {
|
|
252
|
+
startSpinner("Fetching organizations");
|
|
253
|
+
const organizations = opts.org
|
|
254
|
+
? await apiClient.org.list({ query: opts.org })
|
|
255
|
+
: await apiClient.org.list();
|
|
256
|
+
clearSpinner();
|
|
257
|
+
if (opts.org) {
|
|
258
|
+
const exactIdMatch = organizations.find((org) => org.id === opts.org);
|
|
259
|
+
if (exactIdMatch)
|
|
260
|
+
return exactIdMatch;
|
|
261
|
+
if (organizations.length === 0) {
|
|
262
|
+
throw new Error(`Organization not found: ${opts.org}`);
|
|
263
|
+
}
|
|
264
|
+
if (organizations.length === 1)
|
|
265
|
+
return organizations[0];
|
|
266
|
+
return promptOrg(organizations, { allowCreate: false });
|
|
267
|
+
}
|
|
268
|
+
if (organizations.length === 0) {
|
|
269
|
+
if (whenEmpty === "error") {
|
|
270
|
+
throw new Error("You don't belong to any organizations.");
|
|
271
|
+
}
|
|
272
|
+
if (whenEmpty === "prompt-create")
|
|
273
|
+
return promptCreateOrganization();
|
|
274
|
+
return createOrgInline();
|
|
275
|
+
}
|
|
276
|
+
if (organizations.length === 1)
|
|
277
|
+
return organizations[0];
|
|
278
|
+
return promptOrg(organizations, { allowCreate: whenEmpty !== "error" });
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Resolves an API key owned by an organization from `--key` or an interactive
|
|
282
|
+
* prompt.
|
|
283
|
+
*
|
|
284
|
+
* @param organizationId - Organization whose API keys may be selected.
|
|
285
|
+
* @param opts - Parsed API key selector.
|
|
286
|
+
*/
|
|
287
|
+
export async function useOrganizationApiKey(organizationId, opts) {
|
|
288
|
+
startSpinner("Fetching API keys");
|
|
289
|
+
const apiKeys = await apiClient.apiKey.list({ organizationId });
|
|
290
|
+
clearSpinner();
|
|
291
|
+
if (apiKeys.length === 0) {
|
|
292
|
+
throw new Error(`No API keys. Create one with \`${commandExamples.orgApiKeyCreate}\``);
|
|
293
|
+
}
|
|
294
|
+
if (opts.key) {
|
|
295
|
+
const matches = apiKeys.filter((apiKey) => apiKey.id === opts.key ||
|
|
296
|
+
apiKey.start === opts.key ||
|
|
297
|
+
apiKey.name === opts.key);
|
|
298
|
+
if (matches.length === 0) {
|
|
299
|
+
throw new Error(`API key not found: ${opts.key}`);
|
|
300
|
+
}
|
|
301
|
+
if (matches.length === 1)
|
|
302
|
+
return matches[0];
|
|
303
|
+
return promptOrganizationApiKey(matches);
|
|
304
|
+
}
|
|
305
|
+
if (apiKeys.length === 1)
|
|
306
|
+
return apiKeys[0];
|
|
307
|
+
return promptOrganizationApiKey(apiKeys);
|
|
308
|
+
}
|
|
309
|
+
export async function promptOrg(organizations, { allowCreate = true } = {}) {
|
|
310
|
+
if (organizations.length === 0)
|
|
311
|
+
return promptCreateOrganization();
|
|
312
|
+
const orgOptions = organizations.map((o) => ({
|
|
313
|
+
label: `${o.plan === "pro" ? "★ " : ""}${o.name} ${dimmed(o.id)}`,
|
|
314
|
+
value: o.id,
|
|
315
|
+
}));
|
|
316
|
+
const selected = await safelyPrompt(() => select({
|
|
317
|
+
message: "Which organization?",
|
|
318
|
+
options: allowCreate
|
|
319
|
+
? [...orgOptions, { label: "Create a new organization", value: null }]
|
|
320
|
+
: orgOptions,
|
|
321
|
+
}), "--org");
|
|
322
|
+
if (selected == null)
|
|
323
|
+
return createOrgInline();
|
|
324
|
+
return organizations.find((o) => o.id === selected);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Prompts for one API key from an organization-scoped list.
|
|
328
|
+
*
|
|
329
|
+
* @param apiKeys - API keys available for selection.
|
|
330
|
+
*/
|
|
331
|
+
async function promptOrganizationApiKey(apiKeys) {
|
|
332
|
+
const selected = await safelyPrompt(() => select({
|
|
333
|
+
message: "Which API key?",
|
|
334
|
+
options: apiKeys.map((apiKey) => ({
|
|
335
|
+
label: `${apiKey.name ?? "Unnamed"} ${dimmed(apiKey.start ?? apiKey.id)}`,
|
|
336
|
+
value: apiKey.id,
|
|
337
|
+
})),
|
|
338
|
+
}), "--key");
|
|
339
|
+
return apiKeys.find((apiKey) => apiKey.id === selected);
|
|
340
|
+
}
|
|
341
|
+
/** Walks the user through creating an organization. */
|
|
342
|
+
async function createOrgInline() {
|
|
343
|
+
const session = await requireAuthenticatedSession("create an organization");
|
|
344
|
+
const defaultName = session.user.name &&
|
|
345
|
+
!session.user.isAnonymous &&
|
|
346
|
+
!z.email().safeParse(session.user.name).success
|
|
347
|
+
? `${session.user.name}'s Organization`
|
|
348
|
+
: "My Organization";
|
|
349
|
+
startSpinner("Creating organization");
|
|
350
|
+
const org = await apiClient.org.create({
|
|
351
|
+
name: await safelyPromptTextWithValidation({
|
|
352
|
+
message: "What should we name the organization?",
|
|
353
|
+
placeholder: defaultName,
|
|
354
|
+
defaultValue: defaultName,
|
|
355
|
+
}, REASONABLE_NAME_SCHEMA, "--name"),
|
|
356
|
+
});
|
|
357
|
+
clearSpinner();
|
|
358
|
+
clackLog.success(`Created ${accent(org.name)} ${dimmed(org.id)}`);
|
|
359
|
+
return org;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Asks whether to create the user's first organization, then creates it.
|
|
363
|
+
*
|
|
364
|
+
* @throws {Error} When the user declines creation.
|
|
365
|
+
*/
|
|
366
|
+
export async function promptCreateOrganization() {
|
|
367
|
+
if (!(await safelyPrompt(() => confirm({
|
|
368
|
+
message: "You don't have any organizations. Create one now?",
|
|
369
|
+
initialValue: true,
|
|
370
|
+
}), "organization"))) {
|
|
371
|
+
throw new Error("You don't belong to any organizations.");
|
|
372
|
+
}
|
|
373
|
+
return createOrgInline();
|
|
374
|
+
}
|
|
375
|
+
/** Shared schema fragment for commands that operate on a project. */
|
|
376
|
+
export const projectOptsSchema = z.object({
|
|
377
|
+
project: z.string().optional(),
|
|
378
|
+
});
|
|
379
|
+
/** Shared schema fragment for commands that operate on a config directory. */
|
|
380
|
+
export const configDirOptsSchema = z.object({
|
|
381
|
+
dir: z.string().optional(),
|
|
382
|
+
});
|
|
383
|
+
/**
|
|
384
|
+
* Resolves a project from CLI flags or interactive prompt. Fetches all projects
|
|
385
|
+
* across all orgs so the user doesn't need to specify an org. Precedence:
|
|
386
|
+
* `--project` flag > auto-select (single project) > prompt (with inline
|
|
387
|
+
* creation).
|
|
388
|
+
*
|
|
389
|
+
* @param opts - Parsed project CLI options.
|
|
390
|
+
* @param options - Project resolution behavior.
|
|
391
|
+
* @param options.mode - Whether interactive project creation is allowed.
|
|
392
|
+
*/
|
|
393
|
+
export async function useProject(opts, { mode = "select-or-create" } = {}) {
|
|
394
|
+
startSpinner("Fetching projects");
|
|
395
|
+
const allProjects = opts.project
|
|
396
|
+
? await apiClient.project.list({ query: opts.project })
|
|
397
|
+
: await apiClient.project.list();
|
|
398
|
+
clearSpinner();
|
|
399
|
+
if (opts.project) {
|
|
400
|
+
const exactIdMatch = allProjects.find((project) => project.id === opts.project);
|
|
401
|
+
if (exactIdMatch)
|
|
402
|
+
return exactIdMatch;
|
|
403
|
+
if (allProjects.length === 0) {
|
|
404
|
+
throw new Error(`Project not found: ${opts.project}`);
|
|
405
|
+
}
|
|
406
|
+
if (allProjects.length === 1)
|
|
407
|
+
return allProjects[0];
|
|
408
|
+
return promptProject(allProjects, { allowCreate: false });
|
|
409
|
+
}
|
|
410
|
+
if (allProjects.length === 1)
|
|
411
|
+
return allProjects[0];
|
|
412
|
+
if (mode !== "select-or-create") {
|
|
413
|
+
if (allProjects.length === 0) {
|
|
414
|
+
throw new Error(`No projects. Create one with \`${commandExamples.projectCreate}\``);
|
|
415
|
+
}
|
|
416
|
+
return promptProject(allProjects, { allowCreate: false });
|
|
417
|
+
}
|
|
418
|
+
const organizations = await apiClient.org.list();
|
|
419
|
+
return promptProject(allProjects, {
|
|
420
|
+
allowCreate: true,
|
|
421
|
+
organizations: organizations.length > 0
|
|
422
|
+
? organizations
|
|
423
|
+
: [await promptCreateOrganization()],
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Interactive project selection prompt showing "Org > Project" format with an
|
|
428
|
+
* option to create a new project inline. When there are no existing projects,
|
|
429
|
+
* skips straight to creation.
|
|
430
|
+
*
|
|
431
|
+
* @param projects - Projects available for selection.
|
|
432
|
+
* @param options - Selection and inline-creation options.
|
|
433
|
+
*/
|
|
434
|
+
async function promptProject(projects, options) {
|
|
435
|
+
if (projects.length === 0) {
|
|
436
|
+
if (!options.allowCreate) {
|
|
437
|
+
throw new Error(`No projects. Create one with \`${commandExamples.projectCreate}\``);
|
|
438
|
+
}
|
|
439
|
+
return createProjectInline(options.organizations);
|
|
440
|
+
}
|
|
441
|
+
const projectOptions = projects.map((p) => ({
|
|
442
|
+
label: `${p.org.name} › ${p.name}`,
|
|
443
|
+
value: p.id,
|
|
444
|
+
}));
|
|
445
|
+
const selected = await safelyPrompt(() => select({
|
|
446
|
+
message: "Which project?",
|
|
447
|
+
options: options.allowCreate
|
|
448
|
+
? [...projectOptions, { label: "Create a new project", value: null }]
|
|
449
|
+
: projectOptions,
|
|
450
|
+
}), "--project");
|
|
451
|
+
if (selected == null && options.allowCreate) {
|
|
452
|
+
return createProjectInline(options.organizations);
|
|
453
|
+
}
|
|
454
|
+
return projects.find((p) => p.id === selected);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Walks the user through creating a project.
|
|
458
|
+
*
|
|
459
|
+
* @param organizations - Organizations that may own the project.
|
|
460
|
+
*/
|
|
461
|
+
async function createProjectInline(organizations) {
|
|
462
|
+
const org = organizations.length === 1
|
|
463
|
+
? organizations[0]
|
|
464
|
+
: await promptOrg(organizations, {
|
|
465
|
+
allowCreate: true,
|
|
466
|
+
});
|
|
467
|
+
startSpinner("Creating project");
|
|
468
|
+
const project = await apiClient.project.create({
|
|
469
|
+
organizationId: org.id,
|
|
470
|
+
name: await safelyPromptTextWithValidation({
|
|
471
|
+
message: "What should we name the project?",
|
|
472
|
+
}, REASONABLE_NAME_SCHEMA, "--name"),
|
|
473
|
+
});
|
|
474
|
+
clearSpinner();
|
|
475
|
+
clackLog.success(`Created ${accent(project.name)} ${dimmed(project.id)}`);
|
|
476
|
+
return { ...project, org };
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Resolves the config directory from a positional directory or config search.
|
|
480
|
+
* Precedence: explicit directory > current directory > nearest parent >
|
|
481
|
+
* downward search. Multiple downward matches are selected interactively.
|
|
482
|
+
*
|
|
483
|
+
* @param opts - Parsed config-directory CLI options.
|
|
484
|
+
*/
|
|
485
|
+
export async function useConfigDir(opts) {
|
|
486
|
+
if (opts.dir)
|
|
487
|
+
return findExplicitConfigDirMatch(opts.dir);
|
|
488
|
+
const matches = await findConfigDirMatches();
|
|
489
|
+
if (matches.length === 0) {
|
|
490
|
+
throw new Error("No config file found. Run from a project directory or pass a directory explicitly.");
|
|
491
|
+
}
|
|
492
|
+
if (matches.length === 1)
|
|
493
|
+
return matches[0];
|
|
494
|
+
return promptConfigDir(matches);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Prompts the user to select from multiple matching config directories.
|
|
498
|
+
*
|
|
499
|
+
* @param matches - Config directory matches discovered by the search.
|
|
500
|
+
*/
|
|
501
|
+
async function promptConfigDir(matches) {
|
|
502
|
+
const cwd = process.cwd();
|
|
503
|
+
const selected = await safelyPrompt(() => select({
|
|
504
|
+
message: "Which config directory?",
|
|
505
|
+
options: matches.map((match) => ({
|
|
506
|
+
label: formatRelativePath(match.dir, cwd),
|
|
507
|
+
value: match.dir,
|
|
508
|
+
})),
|
|
509
|
+
}), "--dir");
|
|
510
|
+
return matches.find((match) => match.dir === selected);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Formats a path relative to the current working directory.
|
|
514
|
+
*
|
|
515
|
+
* @param target - Absolute path to display.
|
|
516
|
+
* @param cwd - Working directory used as the relative base.
|
|
517
|
+
*/
|
|
518
|
+
function formatRelativePath(target, cwd) {
|
|
519
|
+
const relative = path.relative(cwd, target);
|
|
520
|
+
return relative.length > 0 ? relative : ".";
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Parses `ctx.args`, invokes the command handler, and reports failures.
|
|
524
|
+
*
|
|
525
|
+
* @param optsSchema - Optional schema for parsing Citty arguments.
|
|
526
|
+
* @param cb - Command handler receiving parsed options when present.
|
|
527
|
+
*/
|
|
528
|
+
export function createCommandRun(optsSchema, cb) {
|
|
529
|
+
return async (ctx) => {
|
|
530
|
+
try {
|
|
531
|
+
await cb(await optsSchema?.parseAsync(ctx.args));
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
clearSpinner();
|
|
535
|
+
if (isJsonOutput()) {
|
|
536
|
+
writeJsonError(err);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
// zod errors have a leading "✖ " that we don't want to print
|
|
540
|
+
clackLog.message(red(`${formatErrorMessage(err).replace(/^✖ ?/, "")}\n`), {
|
|
541
|
+
output: process.stderr,
|
|
542
|
+
symbol: red("✖"),
|
|
543
|
+
});
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Selects a simple English singular or plural word form.
|
|
550
|
+
*
|
|
551
|
+
* @param count - Quantity determining which form to use.
|
|
552
|
+
* @param singularWord - Singular noun to pluralize when needed.
|
|
553
|
+
*/
|
|
554
|
+
export function pluralize(count, singularWord) {
|
|
555
|
+
if (count === 1)
|
|
556
|
+
return singularWord;
|
|
557
|
+
if (singularWord.endsWith("y") && !/[aeiou]y$/i.test(singularWord)) {
|
|
558
|
+
return singularWord.slice(0, -1) + "ies";
|
|
559
|
+
}
|
|
560
|
+
return `${singularWord}s`;
|
|
561
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ProjectConfig {
|
|
2
|
+
projectId: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Preserves a project's configuration while providing type checking to config
|
|
6
|
+
* files.
|
|
7
|
+
*
|
|
8
|
+
* @param config - Project configuration to expose to the CLI.
|
|
9
|
+
*/
|
|
10
|
+
export declare function defineConfig(config: ProjectConfig): ProjectConfig;
|
|
11
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,iBAEjD"}
|
package/dist/config.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./automation/runtime.js";
|
|
2
|
+
export * from "./config.js";
|
|
3
|
+
export * from "./automation/signal.js";
|
|
4
|
+
export * from "./automation/subscribe.js";
|
|
5
|
+
export * from "./automation/automation.js";
|
|
6
|
+
export * from "./automation/actions.js";
|
|
7
|
+
export * from "./automation/integrations.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iCAA6B;AAE7B,4BAAwB;AACxB,uCAAmC;AACnC,0CAAsC;AACtC,2CAAuC;AACvC,wCAAoC;AACpC,6CAAyC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import "./automation/runtime.js";
|
|
2
|
+
export * from "./config.js";
|
|
3
|
+
export * from "./automation/signal.js";
|
|
4
|
+
export * from "./automation/subscribe.js";
|
|
5
|
+
export * from "./automation/automation.js";
|
|
6
|
+
export * from "./automation/actions.js";
|
|
7
|
+
export * from "./automation/integrations.js";
|