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,52 @@
|
|
|
1
|
+
import { green, red, yellow } from "ansis";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { joinURL } from "ufo";
|
|
4
|
+
import { commandNames } from "../command-names.js";
|
|
5
|
+
import { renderDetailBlock } from "../lib/detail-block.js";
|
|
6
|
+
import { env } from "../lib/env.js";
|
|
7
|
+
import { createCommandRun, output, sessionIntro, useSession } from "../utils.js";
|
|
8
|
+
import { log } from "@clack/prompts";
|
|
9
|
+
import { dimmed } from "../lib/io.js";
|
|
10
|
+
export const debugCommand = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: commandNames.debug,
|
|
13
|
+
description: "Print resolved app URL and API endpoints this CLI uses",
|
|
14
|
+
},
|
|
15
|
+
run: createCommandRun(null, debug),
|
|
16
|
+
});
|
|
17
|
+
/** Logs APP_ORIGIN and the RPC/auth base URLs derived from it. */
|
|
18
|
+
export async function debug() {
|
|
19
|
+
const { APP_ORIGIN } = env;
|
|
20
|
+
const session = await useSession({ signInAnonymously: false });
|
|
21
|
+
const appOrigin = APP_ORIGIN;
|
|
22
|
+
const apiRpc = joinURL(APP_ORIGIN, "/api/rpc");
|
|
23
|
+
const apiRest = joinURL(APP_ORIGIN, "/api/v1");
|
|
24
|
+
const apiAuth = joinURL(APP_ORIGIN, "/api/auth");
|
|
25
|
+
output
|
|
26
|
+
.normal(() => {
|
|
27
|
+
sessionIntro(session);
|
|
28
|
+
log.message(renderDetailBlock({
|
|
29
|
+
[dimmed `App origin`]: appOrigin,
|
|
30
|
+
[dimmed `API RPC`]: apiRpc,
|
|
31
|
+
[dimmed `API REST`]: apiRest,
|
|
32
|
+
[dimmed `API Auth`]: apiAuth,
|
|
33
|
+
[dimmed `Auth Status`]: session
|
|
34
|
+
? session.user.isAnonymous
|
|
35
|
+
? yellow `Anon`
|
|
36
|
+
: green `Logged in`
|
|
37
|
+
: red `No Auth`,
|
|
38
|
+
}, { keyAlignment: "right" }));
|
|
39
|
+
log.message();
|
|
40
|
+
})
|
|
41
|
+
.json({
|
|
42
|
+
appOrigin,
|
|
43
|
+
apiRpc,
|
|
44
|
+
apiRest,
|
|
45
|
+
apiAuth,
|
|
46
|
+
authStatus: !session
|
|
47
|
+
? "no_auth"
|
|
48
|
+
: session.user.isAnonymous
|
|
49
|
+
? "anonymous"
|
|
50
|
+
: "logged_in",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { apiClient } from "../lib/api-client.js";
|
|
3
|
+
import { configDirOptsSchema } from "../utils.js";
|
|
4
|
+
export declare const deployCommand: import("citty").CommandDef<{
|
|
5
|
+
readonly dir: {
|
|
6
|
+
readonly type: "string";
|
|
7
|
+
readonly description: "Directory containing a config file";
|
|
8
|
+
readonly alias: ["d"];
|
|
9
|
+
};
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Builds and deploys every automation found in a project's config directory.
|
|
13
|
+
*
|
|
14
|
+
* @param opts - Validated options selecting the project directory.
|
|
15
|
+
* @throws When the directory contains no automation source files.
|
|
16
|
+
*/
|
|
17
|
+
export declare function deploy(opts: z.infer<typeof configDirOptsSchema>): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Formats the current deployment status for the interactive spinner.
|
|
20
|
+
*
|
|
21
|
+
* @param deployment - Current deployment polling response.
|
|
22
|
+
*/
|
|
23
|
+
export declare function deploymentStatusMessage(deployment: Pick<Awaited<ReturnType<typeof apiClient.deployment.get>>, "status">): "Deploying" | "Planning automations" | "Creating runtime snapshots" | "Configuring event sources";
|
|
24
|
+
//# sourceMappingURL=deploy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/deploy.ts"],"names":[],"mappings":"AAMA,OAAO,CAAC,MAAM,KAAK,CAAA;AAEnB,OAAO,EAAE,SAAS,EAAE,6BAAyB;AAI7C,OAAO,EAEL,mBAAmB,EAOpB,oBAAgB;AAkBjB,eAAO,MAAM,aAAa;;;;;;EAaxB,CAAA;AAEF;;;;;GAKG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,iBAoErE;AA+CD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,IAAI,CACd,OAAO,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EACpD,QAAQ,CACT,qGASF"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { confirm, log, password, select, text } from "@clack/prompts";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { setTimeout } from "node:timers/promises";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import z from "zod";
|
|
8
|
+
import { commandNames } from "../command-names.js";
|
|
9
|
+
import { apiClient } from "../lib/api-client.js";
|
|
10
|
+
import { accent, dimmed } from "../lib/io.js";
|
|
11
|
+
import { buildAutomations, findAutomationFiles } from "../lib/automation-build.js";
|
|
12
|
+
import { loadProjectConfig } from "../lib/project-config.js";
|
|
13
|
+
import { createCommandRun, configDirOptsSchema, output, pluralize, sessionIntro, safelyPrompt, useConfigDir, useSession, } from "../utils.js";
|
|
14
|
+
import { clearSpinner, startSpinner } from "../lib/spinner.js";
|
|
15
|
+
import { renderDetailBlock } from "../lib/detail-block.js";
|
|
16
|
+
import { isJsonOutput } from "../lib/json-output.js";
|
|
17
|
+
const CONNECT_NEW_ACCOUNT = "__connect_new_account__";
|
|
18
|
+
const AUTHORIZATION_POLL_INTERVAL_MS = 2_000;
|
|
19
|
+
export const deployCommand = defineCommand({
|
|
20
|
+
meta: {
|
|
21
|
+
name: commandNames.deploy,
|
|
22
|
+
description: "Deploy project code",
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
dir: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Directory containing a config file",
|
|
28
|
+
alias: ["d"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
run: createCommandRun(configDirOptsSchema, deploy),
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Builds and deploys every automation found in a project's config directory.
|
|
35
|
+
*
|
|
36
|
+
* @param opts - Validated options selecting the project directory.
|
|
37
|
+
* @throws When the directory contains no automation source files.
|
|
38
|
+
*/
|
|
39
|
+
export async function deploy(opts) {
|
|
40
|
+
const session = await useSession();
|
|
41
|
+
output.normal(() => sessionIntro(session));
|
|
42
|
+
const configDir = await useConfigDir(opts);
|
|
43
|
+
const { projectId } = await loadProjectConfig(configDir.configPath);
|
|
44
|
+
const automationFiles = await findAutomationFiles(configDir.dir);
|
|
45
|
+
if (automationFiles.length === 0) {
|
|
46
|
+
throw new Error(`No automation files found in ${formatDisplayPath(configDir.dir)}. Expected at least one *.automation.ts file.`);
|
|
47
|
+
}
|
|
48
|
+
startSpinner("Building automations");
|
|
49
|
+
const build = await buildAutomations(configDir.dir, automationFiles);
|
|
50
|
+
clearSpinner();
|
|
51
|
+
startSpinner("Deploying");
|
|
52
|
+
const { id: deploymentId } = await apiClient.deployment.create({
|
|
53
|
+
projectId,
|
|
54
|
+
automations: build.automations,
|
|
55
|
+
});
|
|
56
|
+
let deployment = await pollForDeployment(projectId, deploymentId);
|
|
57
|
+
clearSpinner();
|
|
58
|
+
while (deployment.status === "awaiting_authorization") {
|
|
59
|
+
const authorization = await apiClient.authorization.get({
|
|
60
|
+
deploymentId,
|
|
61
|
+
});
|
|
62
|
+
if (isJsonOutput()) {
|
|
63
|
+
output.json({
|
|
64
|
+
...configDir,
|
|
65
|
+
authorization,
|
|
66
|
+
automationFiles,
|
|
67
|
+
build,
|
|
68
|
+
deployment,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
await configureAuthorization(deploymentId, authorization);
|
|
73
|
+
deployment = await pollForDeployment(projectId, deploymentId);
|
|
74
|
+
clearSpinner();
|
|
75
|
+
}
|
|
76
|
+
output.normal(() => log.info(renderDetailBlock({
|
|
77
|
+
[dimmed `Directory`]: formatDisplayPath(configDir.dir),
|
|
78
|
+
[dimmed `Project`]: `${accent(deployment.project.name)}\n${dimmed(deployment.project.id)}`,
|
|
79
|
+
[dimmed `Automations`]: formatAutomationDetails(build.automations),
|
|
80
|
+
}, { keyAlignment: "right", lineSpacing: 1 })));
|
|
81
|
+
output
|
|
82
|
+
.normal(() => log.success(`Successfully deployed ${accent(String(build.automations.length))} ${pluralize(build.automations.length, "automation")}`))
|
|
83
|
+
.json({ ...configDir, automationFiles, build, deployment });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Polls deployment status until it reaches a terminal state.
|
|
87
|
+
*
|
|
88
|
+
* @param projectId - Project that owns the deployment.
|
|
89
|
+
* @param deploymentId - Deployment to poll.
|
|
90
|
+
*/
|
|
91
|
+
async function pollForDeployment(projectId, deploymentId) {
|
|
92
|
+
let previousMessage = "Deploying";
|
|
93
|
+
const interruptMessage = `Stopped waiting. Deployment ${deploymentId} will continue in the background.`;
|
|
94
|
+
startSpinner(previousMessage, interruptMessage);
|
|
95
|
+
while (true) {
|
|
96
|
+
const deployment = await apiClient.deployment.get({
|
|
97
|
+
deploymentId,
|
|
98
|
+
projectId,
|
|
99
|
+
});
|
|
100
|
+
if (deployment.status === "active" || deployment.status === "inactive") {
|
|
101
|
+
return deployment;
|
|
102
|
+
}
|
|
103
|
+
if (deployment.status === "awaiting_authorization")
|
|
104
|
+
return deployment;
|
|
105
|
+
if (deployment.status === "failed") {
|
|
106
|
+
throw new Error(deployment.job?.error ??
|
|
107
|
+
`Deployment failed after ${deployment.job?.attempts ?? 0} attempts.`);
|
|
108
|
+
}
|
|
109
|
+
if (deployment.status === "superseded") {
|
|
110
|
+
throw new Error("Deployment was superseded by a newer deployment.");
|
|
111
|
+
}
|
|
112
|
+
const message = deploymentStatusMessage(deployment);
|
|
113
|
+
if (message !== previousMessage) {
|
|
114
|
+
startSpinner(message, interruptMessage);
|
|
115
|
+
previousMessage = message;
|
|
116
|
+
}
|
|
117
|
+
await setTimeout(2_000);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Formats the current deployment status for the interactive spinner.
|
|
122
|
+
*
|
|
123
|
+
* @param deployment - Current deployment polling response.
|
|
124
|
+
*/
|
|
125
|
+
export function deploymentStatusMessage(deployment) {
|
|
126
|
+
return deployment.status === "planning"
|
|
127
|
+
? "Planning automations"
|
|
128
|
+
: deployment.status === "snapshotting"
|
|
129
|
+
? "Creating runtime snapshots"
|
|
130
|
+
: deployment.status === "configuring"
|
|
131
|
+
? "Configuring event sources"
|
|
132
|
+
: "Deploying";
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Interactively assigns existing eligible accounts to unmet bindings.
|
|
136
|
+
*
|
|
137
|
+
* @param deploymentId - Deployment whose requirements should be assigned.
|
|
138
|
+
* @param authorization - Current accounts and deployment requirements.
|
|
139
|
+
*/
|
|
140
|
+
async function configureAuthorization(deploymentId, authorization) {
|
|
141
|
+
let current = authorization;
|
|
142
|
+
while (true) {
|
|
143
|
+
const requirement = current.requirements.find(({ satisfied }) => !satisfied);
|
|
144
|
+
if (!requirement)
|
|
145
|
+
return;
|
|
146
|
+
const service = current.services.find((candidate) => candidate.id === requirement.serviceId);
|
|
147
|
+
if (!service) {
|
|
148
|
+
throw new Error(`Integration service ${requirement.serviceId} is unavailable.`);
|
|
149
|
+
}
|
|
150
|
+
const accounts = current.accounts.filter((account) => requirement.eligibleAccountIds.includes(account.id));
|
|
151
|
+
if (accounts.length > 0) {
|
|
152
|
+
const selectedAccountId = await safelyPrompt(() => select({
|
|
153
|
+
message: `Choose an account for ${service.name}/${requirement.binding}`,
|
|
154
|
+
options: [
|
|
155
|
+
...accounts.map((account) => ({
|
|
156
|
+
label: account.label,
|
|
157
|
+
value: account.id,
|
|
158
|
+
})),
|
|
159
|
+
{
|
|
160
|
+
label: `Connect new ${service.name} account`,
|
|
161
|
+
value: CONNECT_NEW_ACCOUNT,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}), "Account");
|
|
165
|
+
if (selectedAccountId !== CONNECT_NEW_ACCOUNT) {
|
|
166
|
+
await apiClient.authorization.selectAccount({
|
|
167
|
+
accountId: selectedAccountId,
|
|
168
|
+
binding: requirement.binding,
|
|
169
|
+
deploymentId,
|
|
170
|
+
});
|
|
171
|
+
current = await apiClient.authorization.get({ deploymentId });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
current = await connectAccount(deploymentId, requirement, service);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Connects and selects a new account using a server-provided manifest.
|
|
180
|
+
*
|
|
181
|
+
* @param deploymentId - Deployment awaiting the account.
|
|
182
|
+
* @param requirement - Binding that needs an account.
|
|
183
|
+
* @param service - API-provided service presentation.
|
|
184
|
+
*/
|
|
185
|
+
async function connectAccount(deploymentId, requirement, service) {
|
|
186
|
+
if (service.connection.type === "form") {
|
|
187
|
+
const data = {};
|
|
188
|
+
for (const field of service.connection.fields) {
|
|
189
|
+
const message = field.description
|
|
190
|
+
? `${field.label} — ${field.description}`
|
|
191
|
+
: field.label;
|
|
192
|
+
if (field.input === "boolean") {
|
|
193
|
+
data[field.name] = await safelyPrompt(() => confirm({
|
|
194
|
+
initialValue: field.default,
|
|
195
|
+
message,
|
|
196
|
+
}), field.label);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const options = {
|
|
200
|
+
message,
|
|
201
|
+
placeholder: field.placeholder,
|
|
202
|
+
validate: (value) => {
|
|
203
|
+
if (field.required && !value)
|
|
204
|
+
return `${field.label} is required.`;
|
|
205
|
+
if (field.input === "number" &&
|
|
206
|
+
value &&
|
|
207
|
+
!Number.isFinite(Number(value))) {
|
|
208
|
+
return `${field.label} must be a number.`;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
const value = await safelyPrompt(() => field.input === "password"
|
|
213
|
+
? password(options)
|
|
214
|
+
: text({
|
|
215
|
+
...options,
|
|
216
|
+
initialValue: field.default?.toString(),
|
|
217
|
+
}), field.label);
|
|
218
|
+
if (!value && !field.required)
|
|
219
|
+
continue;
|
|
220
|
+
data[field.name] =
|
|
221
|
+
field.input === "number" ? z.coerce.number().parse(value) : value;
|
|
222
|
+
}
|
|
223
|
+
startSpinner(`Connecting ${service.name}`);
|
|
224
|
+
await apiClient.authorization.submitCredentials({
|
|
225
|
+
binding: requirement.binding,
|
|
226
|
+
data,
|
|
227
|
+
deploymentId,
|
|
228
|
+
serviceId: requirement.serviceId,
|
|
229
|
+
});
|
|
230
|
+
clearSpinner();
|
|
231
|
+
return apiClient.authorization.get({ deploymentId });
|
|
232
|
+
}
|
|
233
|
+
const { url } = await apiClient.authorization.beginConnection({
|
|
234
|
+
binding: requirement.binding,
|
|
235
|
+
deploymentId,
|
|
236
|
+
serviceId: requirement.serviceId,
|
|
237
|
+
});
|
|
238
|
+
log.info(`Authorize ${service.name} in your browser:\n${url}`);
|
|
239
|
+
await open(url, { wait: false }).catch(() => null);
|
|
240
|
+
startSpinner(`Waiting for ${service.name} authorization`, `Stopped waiting. Deployment ${deploymentId} remains pending authorization.`);
|
|
241
|
+
while (true) {
|
|
242
|
+
const authorization = await apiClient.authorization.get({ deploymentId });
|
|
243
|
+
if (authorization.requirements.find((candidate) => candidate.binding === requirement.binding &&
|
|
244
|
+
candidate.serviceId === requirement.serviceId)?.satisfied) {
|
|
245
|
+
clearSpinner();
|
|
246
|
+
return authorization;
|
|
247
|
+
}
|
|
248
|
+
await setTimeout(AUTHORIZATION_POLL_INTERVAL_MS);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Formats an absolute path relative to the current directory or home when
|
|
253
|
+
* possible.
|
|
254
|
+
*
|
|
255
|
+
* @param target - Filesystem path to display.
|
|
256
|
+
*/
|
|
257
|
+
function formatDisplayPath(target) {
|
|
258
|
+
const absoluteTarget = path.resolve(target);
|
|
259
|
+
const cwdRelative = path.relative(process.cwd(), absoluteTarget);
|
|
260
|
+
if (cwdRelative.length > 0 && isRelativeWithin(cwdRelative)) {
|
|
261
|
+
return `.${path.sep}${cwdRelative}`;
|
|
262
|
+
}
|
|
263
|
+
const homeRelative = path.relative(os.homedir(), absoluteTarget);
|
|
264
|
+
if (homeRelative.length === 0)
|
|
265
|
+
return "~";
|
|
266
|
+
if (isRelativeWithin(homeRelative)) {
|
|
267
|
+
return `~${path.sep}${homeRelative}`;
|
|
268
|
+
}
|
|
269
|
+
return absoluteTarget;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Formats deployed automation identities for a CLI detail block.
|
|
273
|
+
*
|
|
274
|
+
* @param automations - Built automations to list.
|
|
275
|
+
*/
|
|
276
|
+
function formatAutomationDetails(automations) {
|
|
277
|
+
if (automations.length === 0)
|
|
278
|
+
return dimmed `None`;
|
|
279
|
+
return automations
|
|
280
|
+
.map((automation) => dimmed(automation.identityKey))
|
|
281
|
+
.join("\n");
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Checks whether a relative path remains within its base directory.
|
|
285
|
+
*
|
|
286
|
+
* @param relative - Relative path to inspect.
|
|
287
|
+
*/
|
|
288
|
+
function isRelativeWithin(relative) {
|
|
289
|
+
return (relative !== ".." &&
|
|
290
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
291
|
+
!path.isAbsolute(relative));
|
|
292
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
/** Arguments accepted by the login command. */
|
|
3
|
+
declare const loginOptsSchema: z.ZodObject<{
|
|
4
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare const loginCommand: import("citty").CommandDef<{
|
|
7
|
+
readonly email: {
|
|
8
|
+
readonly type: "string";
|
|
9
|
+
readonly description: "Prefill the browser login email";
|
|
10
|
+
readonly alias: ["e"];
|
|
11
|
+
};
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Runs the login flow for the given CLI options.
|
|
15
|
+
*
|
|
16
|
+
* @param opts - Validated login options, including an optional email hint.
|
|
17
|
+
*/
|
|
18
|
+
export declare function login(opts: z.infer<typeof loginOptsSchema>): Promise<void>;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/login.ts"],"names":[],"mappings":"AAMA,OAAO,CAAC,MAAM,KAAK,CAAA;AAiBnB,+CAA+C;AAC/C,QAAA,MAAM,eAAe;;iBAEnB,CAAA;AAEF,eAAO,MAAM,YAAY;;;;;;EAavB,CAAA;AAEF;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,iBAKhE"}
|
|
@@ -0,0 +1,145 @@
|
|
|
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 { pick } from "remeda";
|
|
6
|
+
import { joinURL, withQuery } from "ufo";
|
|
7
|
+
import z from "zod";
|
|
8
|
+
import { commandNames } from "../command-names.js";
|
|
9
|
+
import { apiClient } from "../lib/api-client.js";
|
|
10
|
+
import { authClient } from "../lib/auth-client.js";
|
|
11
|
+
import { env } from "../lib/env.js";
|
|
12
|
+
import { clearSavedTokenSync, createCommandRun, output, sessionIntro, saveTokenSync, useSession, } from "../utils.js";
|
|
13
|
+
import { accent } from "../lib/io.js";
|
|
14
|
+
import { clearSpinner, startSpinner } from "../lib/spinner.js";
|
|
15
|
+
import { blue } from "ansis";
|
|
16
|
+
/** Arguments accepted by the login command. */
|
|
17
|
+
const loginOptsSchema = z.object({
|
|
18
|
+
email: z.email().optional(),
|
|
19
|
+
});
|
|
20
|
+
export const loginCommand = defineCommand({
|
|
21
|
+
meta: {
|
|
22
|
+
name: commandNames.login,
|
|
23
|
+
description: "Authenticate with Automate.ax",
|
|
24
|
+
},
|
|
25
|
+
args: {
|
|
26
|
+
email: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Prefill the browser login email",
|
|
29
|
+
alias: ["e"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
run: createCommandRun(loginOptsSchema, login),
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Runs the login flow for the given CLI options.
|
|
36
|
+
*
|
|
37
|
+
* @param opts - Validated login options, including an optional email hint.
|
|
38
|
+
*/
|
|
39
|
+
export async function login(opts) {
|
|
40
|
+
const session = await useSession({ signInAnonymously: false });
|
|
41
|
+
output.normal(() => sessionIntro(session, { skipAnonWarning: true }));
|
|
42
|
+
await loginWithDeviceAuth(opts.email, !!session?.user.isAnonymous);
|
|
43
|
+
}
|
|
44
|
+
const DEVICE_CLIENT_ID = "automate-cli";
|
|
45
|
+
/**
|
|
46
|
+
* Opens the browser to a server-hosted login page, then polls until that page
|
|
47
|
+
* approves the device code.
|
|
48
|
+
*
|
|
49
|
+
* If the CLI currently holds an anonymous session, app-owned rows are migrated
|
|
50
|
+
* to the newly authenticated user before the old session is discarded.
|
|
51
|
+
*
|
|
52
|
+
* @param email - Optional email address to prefill in the browser.
|
|
53
|
+
* @param hasAnonymousSession - Whether to link the current anonymous account.
|
|
54
|
+
* @throws When device authorization or the resulting login session fails.
|
|
55
|
+
*/
|
|
56
|
+
async function loginWithDeviceAuth(email, hasAnonymousSession) {
|
|
57
|
+
startSpinner("Preparing login");
|
|
58
|
+
const { device_code, user_code } = await authClient.device.code({
|
|
59
|
+
client_id: DEVICE_CLIENT_ID,
|
|
60
|
+
});
|
|
61
|
+
const authPageUrl = withQuery(joinURL(env.APP_ORIGIN, "/login"), {
|
|
62
|
+
user_code,
|
|
63
|
+
email: email?.toLowerCase(),
|
|
64
|
+
});
|
|
65
|
+
clearSpinner();
|
|
66
|
+
output.normal(() => log.message(blue.underline.link(authPageUrl, "Open in browser"), {
|
|
67
|
+
symbol: blue `↗`,
|
|
68
|
+
}));
|
|
69
|
+
await open(authPageUrl);
|
|
70
|
+
startSpinner("Waiting for authorization");
|
|
71
|
+
const accessToken = await pollForDeviceToken(device_code);
|
|
72
|
+
clearSpinner();
|
|
73
|
+
if (hasAnonymousSession) {
|
|
74
|
+
startSpinner("Linking account");
|
|
75
|
+
await apiClient.session.linkDeviceSession({ newAccessToken: accessToken });
|
|
76
|
+
clearSpinner();
|
|
77
|
+
}
|
|
78
|
+
saveTokenSync(accessToken);
|
|
79
|
+
const session = await authClient.getSession();
|
|
80
|
+
if (!session) {
|
|
81
|
+
clearSavedTokenSync();
|
|
82
|
+
throw new Error("Failed to login");
|
|
83
|
+
}
|
|
84
|
+
output
|
|
85
|
+
.normal(() => log.success(`Logged in as ${accent(session.user.email)}`))
|
|
86
|
+
.json({
|
|
87
|
+
loggedIn: true,
|
|
88
|
+
success: true,
|
|
89
|
+
user: pick(session.user, [
|
|
90
|
+
"id",
|
|
91
|
+
"name",
|
|
92
|
+
"email",
|
|
93
|
+
"emailVerified",
|
|
94
|
+
"image",
|
|
95
|
+
"createdAt",
|
|
96
|
+
"updatedAt",
|
|
97
|
+
]),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Polls the device token endpoint until the user authorizes or the code
|
|
102
|
+
* expires.
|
|
103
|
+
*
|
|
104
|
+
* @param deviceCode - OAuth device code whose authorization is pending.
|
|
105
|
+
* @throws When authorization is denied, expires, or otherwise fails.
|
|
106
|
+
*/
|
|
107
|
+
async function pollForDeviceToken(deviceCode) {
|
|
108
|
+
let interval = 2000;
|
|
109
|
+
while (true) {
|
|
110
|
+
await setTimeout(interval);
|
|
111
|
+
try {
|
|
112
|
+
const { access_token } = await authClient.device.token({
|
|
113
|
+
device_code: deviceCode,
|
|
114
|
+
client_id: DEVICE_CLIENT_ID,
|
|
115
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
116
|
+
fetchOptions: {
|
|
117
|
+
headers: {
|
|
118
|
+
"user-agent": `Automate.ax CLI`,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
return access_token;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
switch (z.object({ error: z.object({ error: z.string() }) }).safeParse(err).data
|
|
126
|
+
?.error.error) {
|
|
127
|
+
case "authorization_pending":
|
|
128
|
+
continue;
|
|
129
|
+
case "slow_down":
|
|
130
|
+
interval *= 1.5;
|
|
131
|
+
continue;
|
|
132
|
+
case "access_denied":
|
|
133
|
+
throw new Error("Authorization was denied by the user", {
|
|
134
|
+
cause: err,
|
|
135
|
+
});
|
|
136
|
+
case "expired_token":
|
|
137
|
+
throw new Error("Device code expired. Please try again", {
|
|
138
|
+
cause: err,
|
|
139
|
+
});
|
|
140
|
+
default:
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/logout.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,aAAa,qDAMxB,CAAA;AAEF,wDAAwD;AACxD,wBAAsB,MAAM,kBA0B3B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { commandExamples, commandNames } from "../command-names.js";
|
|
4
|
+
import { authClient } from "../lib/auth-client.js";
|
|
5
|
+
import { accent } from "../lib/io.js";
|
|
6
|
+
import { clearSpinner, startSpinner } from "../lib/spinner.js";
|
|
7
|
+
import { clearSavedTokenSync, createCommandRun, output, sessionIntro, useSession, } from "../utils.js";
|
|
8
|
+
export const logoutCommand = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: commandNames.logout,
|
|
11
|
+
description: "Logout from Automate.ax",
|
|
12
|
+
},
|
|
13
|
+
run: createCommandRun(null, logout),
|
|
14
|
+
});
|
|
15
|
+
/** Signs out and clears the saved CLI session token. */
|
|
16
|
+
export async function logout() {
|
|
17
|
+
const session = await useSession({ signInAnonymously: false });
|
|
18
|
+
output.normal(() => sessionIntro(session, { skipAnonWarning: true }));
|
|
19
|
+
if (!session || session.user.isAnonymous) {
|
|
20
|
+
output
|
|
21
|
+
.normal(() => log.warn(`Not currently signed in. Run ${accent(commandExamples.login)} to sign in.`))
|
|
22
|
+
.json({ loggedIn: false, success: false, user: null });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
startSpinner("Signing out");
|
|
26
|
+
await authClient.signOut();
|
|
27
|
+
clearSavedTokenSync();
|
|
28
|
+
clearSpinner();
|
|
29
|
+
output
|
|
30
|
+
.normal(() => log.success(`Signed out. Sign back in with ${accent(commandExamples.login)}`))
|
|
31
|
+
.json({ loggedIn: false, success: true, user: null });
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/me.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,SAAS,qDAMpB,CAAA;AAEF,2DAA2D;AAC3D,wBAAsB,EAAE,kBAyCvB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { intro, log } from "@clack/prompts";
|
|
2
|
+
import { green, red } from "ansis";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { pick } from "remeda";
|
|
5
|
+
import { commandNames } from "../command-names.js";
|
|
6
|
+
import { renderDetailBlock } from "../lib/detail-block.js";
|
|
7
|
+
import { dimmed } from "../lib/io.js";
|
|
8
|
+
import { CLI_PREFIX, createCommandRun, output, useSession } from "../utils.js";
|
|
9
|
+
export const meCommand = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: commandNames.me,
|
|
12
|
+
description: "Check which account is currently logged in",
|
|
13
|
+
},
|
|
14
|
+
run: createCommandRun(null, me),
|
|
15
|
+
});
|
|
16
|
+
/** Prints auth status and identity with the CLI header. */
|
|
17
|
+
export async function me() {
|
|
18
|
+
const session = await useSession({
|
|
19
|
+
signInAnonymously: false,
|
|
20
|
+
});
|
|
21
|
+
output
|
|
22
|
+
.normal(() => {
|
|
23
|
+
intro(CLI_PREFIX);
|
|
24
|
+
log.message(renderDetailBlock(!session || session.user.isAnonymous
|
|
25
|
+
? { [dimmed `Status`]: red `Not logged in` }
|
|
26
|
+
: {
|
|
27
|
+
[dimmed `Status`]: green `Logged in`,
|
|
28
|
+
[dimmed `Name`]: session.user.name,
|
|
29
|
+
[dimmed `Email`]: session.user.email,
|
|
30
|
+
[dimmed `ID`]: dimmed(session.user.id),
|
|
31
|
+
}, { keyAlignment: "right" }));
|
|
32
|
+
log.message();
|
|
33
|
+
})
|
|
34
|
+
.json(!session || session.user.isAnonymous
|
|
35
|
+
? { loggedIn: false, user: null }
|
|
36
|
+
: {
|
|
37
|
+
loggedIn: true,
|
|
38
|
+
user: pick(session.user, [
|
|
39
|
+
"id",
|
|
40
|
+
"name",
|
|
41
|
+
"email",
|
|
42
|
+
"emailVerified",
|
|
43
|
+
"image",
|
|
44
|
+
"createdAt",
|
|
45
|
+
"updatedAt",
|
|
46
|
+
]),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
/** Validates options before an API key secret can be issued. */
|
|
3
|
+
declare const createApiKeyOptsSchema: z.ZodObject<{
|
|
4
|
+
org: z.ZodOptional<z.ZodString>;
|
|
5
|
+
name: z.ZodPrefault<z.ZodNullable<z.ZodString>>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const orgApiKeyCreateCommand: import("citty").CommandDef<{
|
|
8
|
+
readonly name: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "Name for the API key";
|
|
11
|
+
readonly alias: ["n"];
|
|
12
|
+
};
|
|
13
|
+
readonly org: {
|
|
14
|
+
type: "string";
|
|
15
|
+
description: string;
|
|
16
|
+
alias: string[];
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Creates an organization API key and prints its one-time secret.
|
|
21
|
+
*
|
|
22
|
+
* @param opts - Organization selector and API key name.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createOrganizationApiKey(opts: z.infer<typeof createApiKeyOptsSchema>): Promise<void>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=org-api-key-create.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-api-key-create.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/org-api-key-create.ts"],"names":[],"mappings":"AAEA,OAAO,CAAC,MAAM,KAAK,CAAA;AAkBnB,gEAAgE;AAChE,QAAA,MAAM,sBAAsB;;;iBAE1B,CAAA;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;EAcjC,CAAA;AAEF;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,iBA6B7C"}
|