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,171 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
|
+
import open from "open";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
import { commandNames } from "../command-names.js";
|
|
7
|
+
import { authClient } from "../lib/auth-client.js";
|
|
8
|
+
import { env } from "../lib/env.js";
|
|
9
|
+
import { createCommandRun, ORG_OPT, orgOptsSchema, output, requireAuthenticatedSession, sessionIntro, useOrganization, } from "../utils.js";
|
|
10
|
+
import { accent, dimmed } from "../lib/io.js";
|
|
11
|
+
import { isJsonOutput } from "../lib/json-output.js";
|
|
12
|
+
import { clearSpinner, startSpinner, withSpinner } from "../lib/spinner.js";
|
|
13
|
+
import { blue } from "ansis";
|
|
14
|
+
/** Paid plans available through the upgrade command. */
|
|
15
|
+
const upgradePlans = ["pro", "enterprise"];
|
|
16
|
+
/** Arguments accepted by the organization upgrade command. */
|
|
17
|
+
const upgradeOrgOptsSchema = orgOptsSchema.extend({
|
|
18
|
+
plan: z.enum(upgradePlans).optional(),
|
|
19
|
+
});
|
|
20
|
+
export const orgUpgradeCommand = defineCommand({
|
|
21
|
+
meta: {
|
|
22
|
+
name: commandNames.org.upgrade,
|
|
23
|
+
description: "Upgrade an organization plan",
|
|
24
|
+
},
|
|
25
|
+
args: {
|
|
26
|
+
...ORG_OPT,
|
|
27
|
+
plan: {
|
|
28
|
+
type: "enum",
|
|
29
|
+
options: [...upgradePlans],
|
|
30
|
+
description: "Plan to upgrade to",
|
|
31
|
+
alias: ["p"],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
run: createCommandRun(upgradeOrgOptsSchema, upgradeOrganization),
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Starts Checkout for Pro or prints Enterprise contact details.
|
|
38
|
+
*
|
|
39
|
+
* @param opts - Validated organization and target plan options.
|
|
40
|
+
* @param options - Presentation options for callers composing CLI flows.
|
|
41
|
+
* @param options.printSessionIntro - Whether to print the authenticated user.
|
|
42
|
+
*/
|
|
43
|
+
export async function upgradeOrganization(opts, options = {}) {
|
|
44
|
+
const session = await requireAuthenticatedSession("upgrade an organization");
|
|
45
|
+
if (options.printSessionIntro ?? true) {
|
|
46
|
+
output.normal(() => sessionIntro(session));
|
|
47
|
+
}
|
|
48
|
+
const org = await useOrganization(opts);
|
|
49
|
+
const currentPlan = (await withSpinner("Fetching billing details", () => authClient.subscription.list({
|
|
50
|
+
query: {
|
|
51
|
+
referenceId: org.id,
|
|
52
|
+
customerType: "organization",
|
|
53
|
+
},
|
|
54
|
+
}))).find((subscription) => subscription.status === "active" || subscription.status === "trialing")?.plan ?? "free";
|
|
55
|
+
if ((opts.plan ?? (currentPlan === "free" ? "pro" : "enterprise")) ===
|
|
56
|
+
"enterprise") {
|
|
57
|
+
printEnterpriseContact(org);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (currentPlan === "enterprise") {
|
|
61
|
+
output
|
|
62
|
+
.normal(() => log.info(`You're already on Enterprise. Email ${accent `hello@automate.ax`} for plan changes.`))
|
|
63
|
+
.json({ status: "already_enterprise", organization: org });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (currentPlan === "pro") {
|
|
67
|
+
output
|
|
68
|
+
.normal(() => log.info(`You're already on Pro. Run ${accent `${commandNames.root} ${commandNames.org.root} ${commandNames.org.billing}`} to manage billing.`))
|
|
69
|
+
.json({ status: "already_pro", organization: org });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
startSpinner("Opening checkout");
|
|
73
|
+
const { url: checkoutUrl } = await authClient.subscription.upgrade({
|
|
74
|
+
cancelUrl: appUrl("/billing/cli/done?status=cancelled"),
|
|
75
|
+
customerType: "organization",
|
|
76
|
+
disableRedirect: true,
|
|
77
|
+
plan: "pro",
|
|
78
|
+
referenceId: org.id,
|
|
79
|
+
successUrl: appUrl("/billing/cli/done?status=success"),
|
|
80
|
+
});
|
|
81
|
+
clearSpinner();
|
|
82
|
+
const url = requireUrl(checkoutUrl);
|
|
83
|
+
output
|
|
84
|
+
.normal(() => {
|
|
85
|
+
log.success("Checkout ready");
|
|
86
|
+
log.message(`${dimmed `URL:`} ${blue.underline.link(url, "Open checkout")}`, {
|
|
87
|
+
symbol: blue `↗`,
|
|
88
|
+
});
|
|
89
|
+
})
|
|
90
|
+
.json({ url, organization: org, plan: "pro" });
|
|
91
|
+
if (isJsonOutput())
|
|
92
|
+
return;
|
|
93
|
+
await open(url, { wait: false }).catch(() => null);
|
|
94
|
+
startSpinner("Waiting for upgrade");
|
|
95
|
+
await pollForSubscriptionPlan(org.id, "pro");
|
|
96
|
+
clearSpinner();
|
|
97
|
+
log.success(`Upgraded ${accent(org.name)} to Pro`);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Prints and opens the Enterprise upgrade mailto link.
|
|
101
|
+
*
|
|
102
|
+
* @param org - Organization whose details should prefill the message.
|
|
103
|
+
*/
|
|
104
|
+
export function printEnterpriseContact(org) {
|
|
105
|
+
const mailto = enterpriseMailto(org);
|
|
106
|
+
output
|
|
107
|
+
.normal(() => {
|
|
108
|
+
log.info([
|
|
109
|
+
"Enterprise plans are custom.",
|
|
110
|
+
`Email ${accent `hello@automate.ax`} to upgrade ${accent(org.name)}.`,
|
|
111
|
+
].join("\n"));
|
|
112
|
+
log.message(blue.underline.link(mailto, `hello@automate.ax`), {
|
|
113
|
+
symbol: blue `↗`,
|
|
114
|
+
});
|
|
115
|
+
})
|
|
116
|
+
.json({ url: mailto, email: "hello@automate.ax", organization: org });
|
|
117
|
+
if (isJsonOutput())
|
|
118
|
+
return;
|
|
119
|
+
void open(mailto, { wait: false }).catch(() => null);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds an Enterprise sales email link for an organization.
|
|
123
|
+
*
|
|
124
|
+
* @param org - Organization whose details should prefill the message.
|
|
125
|
+
*/
|
|
126
|
+
function enterpriseMailto(org) {
|
|
127
|
+
return `mailto:hello@automate.ax?${new URLSearchParams({
|
|
128
|
+
body: `Organization: ${org.name} (${org.id})`,
|
|
129
|
+
subject: "Automate.ax Enterprise upgrade",
|
|
130
|
+
}).toString()}`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolves an application-relative path against the configured app origin.
|
|
134
|
+
*
|
|
135
|
+
* @param path - Application path to resolve.
|
|
136
|
+
*/
|
|
137
|
+
function appUrl(path) {
|
|
138
|
+
return new URL(path, env.APP_ORIGIN).toString();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Narrows a nullable checkout URL to a usable URL.
|
|
142
|
+
*
|
|
143
|
+
* @param url - Checkout URL returned by the billing provider.
|
|
144
|
+
* @throws When the billing provider does not return a URL.
|
|
145
|
+
*/
|
|
146
|
+
function requireUrl(url) {
|
|
147
|
+
if (!url)
|
|
148
|
+
throw new Error("Could not open checkout. Please try again.");
|
|
149
|
+
return url;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Polls until Stripe webhooks sync the expected organization subscription.
|
|
153
|
+
*
|
|
154
|
+
* @param organizationId - Organization whose subscription should be checked.
|
|
155
|
+
* @param plan - Plan expected after checkout completes.
|
|
156
|
+
*/
|
|
157
|
+
async function pollForSubscriptionPlan(organizationId, plan) {
|
|
158
|
+
while (true) {
|
|
159
|
+
await setTimeout(2000);
|
|
160
|
+
if ((await authClient.subscription.list({
|
|
161
|
+
query: {
|
|
162
|
+
referenceId: organizationId,
|
|
163
|
+
customerType: "organization",
|
|
164
|
+
},
|
|
165
|
+
})).some((subscription) => subscription.plan === plan &&
|
|
166
|
+
(subscription.status === "active" ||
|
|
167
|
+
subscription.status === "trialing"))) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/org.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,mBAAmB,qDAkB9B,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { commandNames } from "../command-names.js";
|
|
3
|
+
import { orgApiKeyCommand } from "./org-api-key.js";
|
|
4
|
+
import { orgBillingCommand } from "./org-billing.js";
|
|
5
|
+
import { orgCreateCommand } from "./org-create.js";
|
|
6
|
+
import { orgDeleteCommand } from "./org-delete.js";
|
|
7
|
+
import { orgInviteCommand } from "./org-invite.js";
|
|
8
|
+
import { orgLeaveCommand } from "./org-leave.js";
|
|
9
|
+
import { orgListCommand } from "./org-list.js";
|
|
10
|
+
import { orgRemoveCommand } from "./org-remove.js";
|
|
11
|
+
import { orgRenameCommand } from "./org-rename.js";
|
|
12
|
+
import { orgShowCommand } from "./org-show.js";
|
|
13
|
+
import { orgUpgradeCommand } from "./org-upgrade.js";
|
|
14
|
+
export const organizationCommand = defineCommand({
|
|
15
|
+
meta: {
|
|
16
|
+
name: commandNames.org.root,
|
|
17
|
+
description: "Manage organizations (including billing)",
|
|
18
|
+
},
|
|
19
|
+
subCommands: {
|
|
20
|
+
[commandNames.org.apiKey.root]: orgApiKeyCommand,
|
|
21
|
+
[commandNames.org.billing]: orgBillingCommand,
|
|
22
|
+
[commandNames.org.create]: orgCreateCommand,
|
|
23
|
+
[commandNames.org.delete]: orgDeleteCommand,
|
|
24
|
+
[commandNames.org.invite]: orgInviteCommand,
|
|
25
|
+
[commandNames.org.leave]: orgLeaveCommand,
|
|
26
|
+
[commandNames.org.list]: orgListCommand,
|
|
27
|
+
[commandNames.org.remove]: orgRemoveCommand,
|
|
28
|
+
[commandNames.org.rename]: orgRenameCommand,
|
|
29
|
+
[commandNames.org.show]: orgShowCommand,
|
|
30
|
+
[commandNames.org.upgrade]: orgUpgradeCommand,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
/** Arguments accepted by the project creation command. */
|
|
3
|
+
declare const createProjectOptsSchema: z.ZodObject<{
|
|
4
|
+
org: z.ZodOptional<z.ZodString>;
|
|
5
|
+
name: z.ZodPrefault<z.ZodNullable<z.ZodString>>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const projectCreateCommand: import("citty").CommandDef<{
|
|
8
|
+
readonly name: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "The name of the project";
|
|
11
|
+
readonly alias: ["n"];
|
|
12
|
+
};
|
|
13
|
+
readonly org: {
|
|
14
|
+
type: "string";
|
|
15
|
+
description: string;
|
|
16
|
+
alias: string[];
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Creates a project in the resolved organization.
|
|
21
|
+
*
|
|
22
|
+
* @param opts - Validated organization selector and optional project name.
|
|
23
|
+
* @throws When the organization has reached its project limit.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createProject(opts: z.infer<typeof createProjectOptsSchema>): Promise<void>;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=project-create.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-create.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/project-create.ts"],"names":[],"mappings":"AACA,OAAO,CAAC,MAAM,KAAK,CAAA;AAmBnB,0DAA0D;AAC1D,QAAA,MAAM,uBAAuB;;;iBAE3B,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;;;EAc/B,CAAA;AAEF;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,iBAkC9C"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { commandNames } from "../command-names.js";
|
|
3
|
+
import { apiClient } from "../lib/api-client.js";
|
|
4
|
+
import { accent, dimmed } from "../lib/io.js";
|
|
5
|
+
import { clearSpinner, startSpinner, withSpinner } from "../lib/spinner.js";
|
|
6
|
+
import { createCommandRun, ORG_OPT, orgOptsSchema, output, promptableOption, REASONABLE_NAME_SCHEMA, safelyPromptTextWithValidation, sessionIntro, useOrganization, useSession, } from "../utils.js";
|
|
7
|
+
import { log } from "@clack/prompts";
|
|
8
|
+
/** Arguments accepted by the project creation command. */
|
|
9
|
+
const createProjectOptsSchema = orgOptsSchema.extend({
|
|
10
|
+
name: promptableOption(REASONABLE_NAME_SCHEMA, "--name"),
|
|
11
|
+
});
|
|
12
|
+
export const projectCreateCommand = defineCommand({
|
|
13
|
+
meta: {
|
|
14
|
+
name: commandNames.project.create,
|
|
15
|
+
description: "Create a new project",
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
...ORG_OPT,
|
|
19
|
+
name: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "The name of the project",
|
|
22
|
+
alias: ["n"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
run: createCommandRun(createProjectOptsSchema, createProject),
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Creates a project in the resolved organization.
|
|
29
|
+
*
|
|
30
|
+
* @param opts - Validated organization selector and optional project name.
|
|
31
|
+
* @throws When the organization has reached its project limit.
|
|
32
|
+
*/
|
|
33
|
+
export async function createProject(opts) {
|
|
34
|
+
const session = await useSession();
|
|
35
|
+
output.normal(() => sessionIntro(session));
|
|
36
|
+
const org = await useOrganization(opts, {
|
|
37
|
+
whenEmpty: session.user.isAnonymous ? "error" : "prompt-create",
|
|
38
|
+
});
|
|
39
|
+
if (!(await withSpinner("Checking project limit", () => apiClient.project.canCreate({ organizationId: org.id })))) {
|
|
40
|
+
throw new Error(`Project limit reached. Run \`${commandNames.root} ${commandNames.org.root} ${commandNames.org.upgrade} -o ${org.id}\` to create more projects.`);
|
|
41
|
+
}
|
|
42
|
+
let name = opts.name;
|
|
43
|
+
if (name == null)
|
|
44
|
+
name = await promptName();
|
|
45
|
+
startSpinner("Creating project");
|
|
46
|
+
const project = await apiClient.project.create({
|
|
47
|
+
organizationId: org.id,
|
|
48
|
+
name,
|
|
49
|
+
});
|
|
50
|
+
clearSpinner();
|
|
51
|
+
output
|
|
52
|
+
.normal(() => log.success(`Created ${accent(project.name)} ${dimmed(project.id)}`))
|
|
53
|
+
.json(project);
|
|
54
|
+
}
|
|
55
|
+
/** Prompts for and validates a project name. */
|
|
56
|
+
function promptName() {
|
|
57
|
+
return safelyPromptTextWithValidation({
|
|
58
|
+
message: "What should we name the project?",
|
|
59
|
+
placeholder: "My Awesome Project",
|
|
60
|
+
}, REASONABLE_NAME_SCHEMA, "--name");
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
/** Arguments accepted by the project deletion command. */
|
|
3
|
+
declare const deleteProjectOptsSchema: z.ZodObject<{
|
|
4
|
+
project: z.ZodOptional<z.ZodString>;
|
|
5
|
+
yes: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const projectDeleteCommand: import("citty").CommandDef<{
|
|
8
|
+
readonly yes: {
|
|
9
|
+
readonly type: "boolean";
|
|
10
|
+
readonly description: "Skip confirmation prompt";
|
|
11
|
+
readonly alias: ["y"];
|
|
12
|
+
readonly default: false;
|
|
13
|
+
};
|
|
14
|
+
readonly project: {
|
|
15
|
+
type: "string";
|
|
16
|
+
description: string;
|
|
17
|
+
alias: string[];
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Deletes a project after confirmation.
|
|
22
|
+
*
|
|
23
|
+
* @param opts - Validated project selector and confirmation options.
|
|
24
|
+
*/
|
|
25
|
+
export declare function deleteProject(opts: z.infer<typeof deleteProjectOptsSchema>): Promise<void>;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=project-delete.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-delete.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/project-delete.ts"],"names":[],"mappings":"AAEA,OAAO,CAAC,MAAM,KAAK,CAAA;AAgBnB,0DAA0D;AAC1D,QAAA,MAAM,uBAAuB;;;iBAE3B,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAe/B,CAAA;AAEF;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,iBAiC9C"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { confirm, log } from "@clack/prompts";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { commandNames } from "../command-names.js";
|
|
5
|
+
import { apiClient } from "../lib/api-client.js";
|
|
6
|
+
import { createCommandRun, output, PROJECT_OPT, projectOptsSchema, safelyPrompt, sessionIntro, useProject, useSession, } from "../utils.js";
|
|
7
|
+
import { accent, dimmed } from "../lib/io.js";
|
|
8
|
+
import { clearSpinner, startSpinner } from "../lib/spinner.js";
|
|
9
|
+
/** Arguments accepted by the project deletion command. */
|
|
10
|
+
const deleteProjectOptsSchema = projectOptsSchema.extend({
|
|
11
|
+
yes: z.boolean().default(false),
|
|
12
|
+
});
|
|
13
|
+
export const projectDeleteCommand = defineCommand({
|
|
14
|
+
meta: {
|
|
15
|
+
name: commandNames.project.delete,
|
|
16
|
+
description: "Delete a project",
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
...PROJECT_OPT,
|
|
20
|
+
yes: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
description: "Skip confirmation prompt",
|
|
23
|
+
alias: ["y"],
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
run: createCommandRun(deleteProjectOptsSchema, deleteProject),
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Deletes a project after confirmation.
|
|
31
|
+
*
|
|
32
|
+
* @param opts - Validated project selector and confirmation options.
|
|
33
|
+
*/
|
|
34
|
+
export async function deleteProject(opts) {
|
|
35
|
+
const session = await useSession();
|
|
36
|
+
output.normal(() => sessionIntro(session));
|
|
37
|
+
const project = await useProject(opts, {
|
|
38
|
+
mode: "select",
|
|
39
|
+
});
|
|
40
|
+
if (!opts.yes) {
|
|
41
|
+
if (!(await safelyPrompt(() => confirm({
|
|
42
|
+
message: `Are you sure you want to delete ${accent(project.name)}? This cannot be undone.`,
|
|
43
|
+
initialValue: false,
|
|
44
|
+
}), "--yes"))) {
|
|
45
|
+
output.json({ cancelled: true });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
startSpinner("Deleting project");
|
|
50
|
+
await apiClient.project.delete({ projectId: project.id });
|
|
51
|
+
clearSpinner();
|
|
52
|
+
output
|
|
53
|
+
.normal(() => log.success(`Deleted ${accent(project.name)} ${dimmed(project.id)}`))
|
|
54
|
+
.json({ deleted: true, project });
|
|
55
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { orgOptsSchema } from "../utils.js";
|
|
3
|
+
export declare const projectListCommand: import("citty").CommandDef<{
|
|
4
|
+
readonly org: {
|
|
5
|
+
type: "string";
|
|
6
|
+
description: string;
|
|
7
|
+
alias: string[];
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Lists all projects across organizations, optionally filtered by `--org`.
|
|
12
|
+
*
|
|
13
|
+
* @param opts - Validated options containing an optional organization filter.
|
|
14
|
+
* @throws When an explicit organization filter matches no organization.
|
|
15
|
+
*/
|
|
16
|
+
export declare function listProjects(opts: z.infer<typeof orgOptsSchema>): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=project-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-list.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/project-list.ts"],"names":[],"mappings":"AAEA,OAAO,CAAC,MAAM,KAAK,CAAA;AAGnB,OAAO,EAGL,aAAa,EAMd,oBAAgB;AAIjB,eAAO,MAAM,kBAAkB;;;;;;EAS7B,CAAA;AAEF;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,iBA8DrE"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { commandExamples, commandNames } from "../command-names.js";
|
|
4
|
+
import { apiClient } from "../lib/api-client.js";
|
|
5
|
+
import { createCommandRun, ORG_OPT, orgOptsSchema, output, pluralize, promptOrg, sessionIntro, useSession, } from "../utils.js";
|
|
6
|
+
import { accent, dimmed, renderTable } from "../lib/io.js";
|
|
7
|
+
import { clearSpinner, startSpinner } from "../lib/spinner.js";
|
|
8
|
+
export const projectListCommand = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: commandNames.project.list,
|
|
11
|
+
description: "List projects",
|
|
12
|
+
},
|
|
13
|
+
args: {
|
|
14
|
+
...ORG_OPT,
|
|
15
|
+
},
|
|
16
|
+
run: createCommandRun(orgOptsSchema, listProjects),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Lists all projects across organizations, optionally filtered by `--org`.
|
|
20
|
+
*
|
|
21
|
+
* @param opts - Validated options containing an optional organization filter.
|
|
22
|
+
* @throws When an explicit organization filter matches no organization.
|
|
23
|
+
*/
|
|
24
|
+
export async function listProjects(opts) {
|
|
25
|
+
const session = await useSession();
|
|
26
|
+
output.normal(() => sessionIntro(session));
|
|
27
|
+
let organizationId;
|
|
28
|
+
if (opts.org) {
|
|
29
|
+
startSpinner("Fetching organizations");
|
|
30
|
+
const organizations = await apiClient.org.list({ query: opts.org });
|
|
31
|
+
clearSpinner();
|
|
32
|
+
if (organizations.length === 0) {
|
|
33
|
+
throw new Error(`Organization not found: ${opts.org}`);
|
|
34
|
+
}
|
|
35
|
+
organizationId = (organizations.find((org) => org.id === opts.org) ??
|
|
36
|
+
(organizations.length === 1
|
|
37
|
+
? organizations[0]
|
|
38
|
+
: await promptOrg(organizations, { allowCreate: false }))).id;
|
|
39
|
+
}
|
|
40
|
+
startSpinner("Fetching projects");
|
|
41
|
+
const allProjects = await apiClient.project.list(organizationId ? { organizationId } : undefined);
|
|
42
|
+
clearSpinner();
|
|
43
|
+
output
|
|
44
|
+
.normal(() => {
|
|
45
|
+
if (allProjects.length === 0) {
|
|
46
|
+
log.warn(`No projects. Create one with ${accent(commandExamples.projectCreate)}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
log.message(`${accent(String(allProjects.length))} ${pluralize(allProjects.length, "project")}`);
|
|
50
|
+
for (const { org, projects } of groupProjectsByOrg(allProjects)) {
|
|
51
|
+
log.message(`${accent(org.name)} - ${projects.length} ${pluralize(projects.length, "project")}`);
|
|
52
|
+
console.log(renderTable({
|
|
53
|
+
columns: [
|
|
54
|
+
{
|
|
55
|
+
header: "Project",
|
|
56
|
+
cell: (project) => `${project.name}\n${dimmed(project.id)}`,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
header: "Created",
|
|
60
|
+
cell: (project) => new Date(project.createdAt).toLocaleDateString(),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
rows: projects,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.json(allProjects);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Groups projects by their organization while retaining encounter order.
|
|
71
|
+
*
|
|
72
|
+
* @param projects - Projects carrying their owning organization.
|
|
73
|
+
*/
|
|
74
|
+
function groupProjectsByOrg(projects) {
|
|
75
|
+
const groups = new Map();
|
|
76
|
+
for (const project of projects) {
|
|
77
|
+
const group = groups.get(project.org.id);
|
|
78
|
+
if (group) {
|
|
79
|
+
group.projects.push(project);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
groups.set(project.org.id, {
|
|
83
|
+
org: project.org,
|
|
84
|
+
projects: [project],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...groups.values()];
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
/** Arguments accepted by the project rename command. */
|
|
3
|
+
declare const renameProjectOptsSchema: z.ZodObject<{
|
|
4
|
+
project: z.ZodOptional<z.ZodString>;
|
|
5
|
+
name: z.ZodPrefault<z.ZodNullable<z.ZodString>>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const projectRenameCommand: import("citty").CommandDef<{
|
|
8
|
+
readonly name: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "New name for the project";
|
|
11
|
+
readonly alias: ["n"];
|
|
12
|
+
};
|
|
13
|
+
readonly project: {
|
|
14
|
+
type: "string";
|
|
15
|
+
description: string;
|
|
16
|
+
alias: string[];
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Renames a project.
|
|
21
|
+
*
|
|
22
|
+
* @param opts - Validated project selector and optional new name.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renameProject(opts: z.infer<typeof renameProjectOptsSchema>): Promise<void>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=project-rename.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-rename.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/project-rename.ts"],"names":[],"mappings":"AAEA,OAAO,CAAC,MAAM,KAAK,CAAA;AAkBnB,wDAAwD;AACxD,QAAA,MAAM,uBAAuB;;;iBAE3B,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;;;EAc/B,CAAA;AAEF;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,iBAqB9C"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { commandNames } from "../command-names.js";
|
|
4
|
+
import { apiClient } from "../lib/api-client.js";
|
|
5
|
+
import { createCommandRun, output, PROJECT_OPT, projectOptsSchema, promptableOption, REASONABLE_NAME_SCHEMA, safelyPromptTextWithValidation, sessionIntro, useProject, useSession, } from "../utils.js";
|
|
6
|
+
import { accent, dimmed } from "../lib/io.js";
|
|
7
|
+
import { clearSpinner, startSpinner } from "../lib/spinner.js";
|
|
8
|
+
/** Arguments accepted by the project rename command. */
|
|
9
|
+
const renameProjectOptsSchema = projectOptsSchema.extend({
|
|
10
|
+
name: promptableOption(REASONABLE_NAME_SCHEMA, "--name"),
|
|
11
|
+
});
|
|
12
|
+
export const projectRenameCommand = defineCommand({
|
|
13
|
+
meta: {
|
|
14
|
+
name: commandNames.project.rename,
|
|
15
|
+
description: "Rename a project",
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
...PROJECT_OPT,
|
|
19
|
+
name: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "New name for the project",
|
|
22
|
+
alias: ["n"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
run: createCommandRun(renameProjectOptsSchema, renameProject),
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Renames a project.
|
|
29
|
+
*
|
|
30
|
+
* @param opts - Validated project selector and optional new name.
|
|
31
|
+
*/
|
|
32
|
+
export async function renameProject(opts) {
|
|
33
|
+
const session = await useSession();
|
|
34
|
+
output.normal(() => sessionIntro(session));
|
|
35
|
+
const project = await useProject(opts, {
|
|
36
|
+
mode: "select",
|
|
37
|
+
});
|
|
38
|
+
const newName = opts.name ?? (await promptName(project.name));
|
|
39
|
+
startSpinner("Renaming project");
|
|
40
|
+
await apiClient.project.update({
|
|
41
|
+
projectId: project.id,
|
|
42
|
+
name: newName,
|
|
43
|
+
});
|
|
44
|
+
clearSpinner();
|
|
45
|
+
output
|
|
46
|
+
.normal(() => log.success(`Renamed to ${accent(newName)} ${dimmed(project.id)}`))
|
|
47
|
+
.json({ ...project, name: newName });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Prompts for and validates a replacement project name.
|
|
51
|
+
*
|
|
52
|
+
* @param currentName - Current project name used as the prompt default.
|
|
53
|
+
*/
|
|
54
|
+
async function promptName(currentName) {
|
|
55
|
+
return await safelyPromptTextWithValidation({
|
|
56
|
+
message: "What should the new project name be?",
|
|
57
|
+
placeholder: currentName,
|
|
58
|
+
initialValue: currentName,
|
|
59
|
+
}, REASONABLE_NAME_SCHEMA, "--name");
|
|
60
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { projectOptsSchema } from "../utils.js";
|
|
3
|
+
export declare const projectShowCommand: import("citty").CommandDef<{
|
|
4
|
+
readonly project: {
|
|
5
|
+
type: "string";
|
|
6
|
+
description: string;
|
|
7
|
+
alias: string[];
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Displays details for a resolved project.
|
|
12
|
+
*
|
|
13
|
+
* @param opts - Validated options selecting the project.
|
|
14
|
+
*/
|
|
15
|
+
export declare function showProject(opts: z.infer<typeof projectOptsSchema>): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=project-show.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-show.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/project-show.ts"],"names":[],"mappings":"AAEA,OAAO,CAAC,MAAM,KAAK,CAAA;AAGnB,OAAO,EAIL,iBAAiB,EAIlB,oBAAgB;AAIjB,eAAO,MAAM,kBAAkB;;;;;;EAS7B,CAAA;AAEF;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,iBAmBxE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { commandNames } from "../command-names.js";
|
|
4
|
+
import { apiClient } from "../lib/api-client.js";
|
|
5
|
+
import { createCommandRun, output, PROJECT_OPT, projectOptsSchema, sessionIntro, useProject, useSession, } from "../utils.js";
|
|
6
|
+
import { accent, dimmed } from "../lib/io.js";
|
|
7
|
+
import { clearSpinner, startSpinner } from "../lib/spinner.js";
|
|
8
|
+
export const projectShowCommand = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: commandNames.project.show,
|
|
11
|
+
description: "Show project details",
|
|
12
|
+
},
|
|
13
|
+
args: {
|
|
14
|
+
...PROJECT_OPT,
|
|
15
|
+
},
|
|
16
|
+
run: createCommandRun(projectOptsSchema, showProject),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Displays details for a resolved project.
|
|
20
|
+
*
|
|
21
|
+
* @param opts - Validated options selecting the project.
|
|
22
|
+
*/
|
|
23
|
+
export async function showProject(opts) {
|
|
24
|
+
const session = await useSession();
|
|
25
|
+
output.normal(() => sessionIntro(session));
|
|
26
|
+
const { id: projectId } = await useProject(opts, { mode: "select" });
|
|
27
|
+
startSpinner("Fetching details");
|
|
28
|
+
const full = await apiClient.project.get({ projectId });
|
|
29
|
+
clearSpinner();
|
|
30
|
+
output
|
|
31
|
+
.normal(() => log.message([
|
|
32
|
+
`${accent(full.name)} ${dimmed(full.id)}`,
|
|
33
|
+
` Organization: ${full.org.name} ${dimmed(full.org.id)}`,
|
|
34
|
+
` Created: ${new Date(full.createdAt).toLocaleDateString()}`,
|
|
35
|
+
].join("\n")))
|
|
36
|
+
.json(full);
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/project.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,cAAc,qDAYzB,CAAA"}
|