@ao-ai/cli 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.local ADDED
@@ -0,0 +1,3 @@
1
+ AO_API_URL=http://localhost:5000
2
+
3
+ AO_WEB_URL=http://localhost:3000
Binary file
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import open from "open";
6
+ import toml from "toml";
7
+ import { z } from "zod";
8
+ import http from "http";
9
+ import { API_BASE_URL, authSuccessMessage, getAPIKey, readConfig, requireAuth, WEB_BASE_URL, writeConfig, zipProject, } from "./utils.js";
10
+ import { Readable } from "stream";
11
+ import chalk from "chalk";
12
+ import keytar from "keytar";
13
+ const program = new Command();
14
+ program.name("ao").description("AO CLI").version("0.0.1");
15
+ program
16
+ .command("init")
17
+ .action(() => {
18
+ const currentDir = process.cwd();
19
+ const filePath = path.join(currentDir, "ao.toml");
20
+ if (fs.existsSync(filePath)) {
21
+ console.log("AO configuration file already exists.");
22
+ return;
23
+ }
24
+ const initialContent = `
25
+ name = "my-project"
26
+
27
+ [runtime]
28
+ max_retries = 3
29
+ timeout = 300 # 5 minutes
30
+ `;
31
+ fs.writeFileSync(filePath, initialContent.trim());
32
+ console.log("Initialized AO configuration file at:", filePath);
33
+ })
34
+ .description("");
35
+ program.command("login").action(async () => {
36
+ const apiKey = await getAPIKey();
37
+ if (apiKey) {
38
+ console.log(chalk.green("You are already logged in."));
39
+ process.exit(0);
40
+ }
41
+ const server = http.createServer(async (req, res) => {
42
+ const address = server.address();
43
+ const port = typeof address === "object" && address ? address.port : 0;
44
+ const url = new URL(req.url, `http://localhost:${port}`);
45
+ const apiKey = url.searchParams.get("apiKey");
46
+ if (apiKey) {
47
+ await keytar.setPassword("ao-cli", "api-key", apiKey);
48
+ res.writeHead(302, {
49
+ Location: `${WEB_BASE_URL}/auth/cli/success`,
50
+ });
51
+ res.end();
52
+ authSuccessMessage();
53
+ server.close();
54
+ process.exit(0);
55
+ }
56
+ });
57
+ await new Promise((resolve) => {
58
+ server.listen(0, resolve);
59
+ });
60
+ const { port } = server.address();
61
+ const authUrl = `${WEB_BASE_URL}/auth/cli/start?redirect=http://localhost:${port}`;
62
+ await open(authUrl);
63
+ });
64
+ program.command("logout").action(async () => {
65
+ await keytar.deletePassword("ao-cli", "api-key");
66
+ console.log(chalk.green("✓ Logged out successfully"));
67
+ });
68
+ program.command("deploy").action(async () => {
69
+ const apiKey = await requireAuth();
70
+ const currentDir = process.cwd();
71
+ const filePath = path.join(currentDir, "ao.toml");
72
+ const pyProjectFilePath = path.join(currentDir, "pyproject.toml");
73
+ const requirementsFilePath = path.join(currentDir, "requirements.txt");
74
+ if (!fs.existsSync(filePath)) {
75
+ console.error("AO configuration file not found. Please run 'ao init' first.");
76
+ return;
77
+ }
78
+ if (!fs.existsSync(requirementsFilePath)) {
79
+ console.error("requirements.txt is required");
80
+ return;
81
+ }
82
+ if (!fs.existsSync(pyProjectFilePath)) {
83
+ console.error("pyproject.toml file not found. Please make sure you have a pyproject.toml file in the current directory.");
84
+ return;
85
+ }
86
+ console.log("Deploying agent...");
87
+ // Get the configuration from ao.toml
88
+ const configContent = fs.readFileSync(filePath, "utf-8");
89
+ // Parse the TOML content and extract necessary information for deployment
90
+ let config;
91
+ try {
92
+ config = toml.parse(configContent);
93
+ }
94
+ catch (error) {
95
+ console.error("Invalid TOML syntax in ao.toml:", error);
96
+ process.exit(1);
97
+ }
98
+ const configFileSchema = z.object({
99
+ name: z
100
+ .string()
101
+ .lowercase()
102
+ .min(3, "The 'name' field must be at least 3 characters long."),
103
+ deploy: z.object({
104
+ entrypoint: z.string(),
105
+ }),
106
+ runtime: z.object({
107
+ max_retries: z.number().int().nonnegative().optional().default(3),
108
+ timeout: z.number().int().positive().optional().default(300),
109
+ }),
110
+ schedule: z
111
+ .object({
112
+ cron: z.string(),
113
+ })
114
+ .optional(),
115
+ });
116
+ // Check if the AO config file structure is valid
117
+ const validation = configFileSchema.safeParse(config);
118
+ if (!validation.success) {
119
+ console.error("Invalid ao.toml structure:");
120
+ console.error(validation.error.message);
121
+ process.exit(1);
122
+ }
123
+ // Zip the agent code and dependencies
124
+ // 1. Node stream → Web stream
125
+ console.log("Packing project...");
126
+ const zipNodeStream = await zipProject(); // Readable (Node)
127
+ const zipWebStream = Readable.toWeb(zipNodeStream);
128
+ // 2. Web stream → Blob (bien tipado)
129
+ const zipBlob = await new Response(zipWebStream).blob();
130
+ const form = new FormData();
131
+ form.append("file", zipBlob, "agent.zip");
132
+ const response = await fetch(`${API_BASE_URL}/deploy`, {
133
+ method: "POST",
134
+ headers: {
135
+ "X-API-Key": `Bearer ${apiKey}`,
136
+ },
137
+ body: form,
138
+ });
139
+ if (!response.ok) {
140
+ console.error("Deployment failed:", response.statusText);
141
+ process.exit(1);
142
+ }
143
+ console.log("Deployment successful!");
144
+ console.log("You can view your new deployment at:", `https://aodeploy.com/dashboard/deployments`);
145
+ });
146
+ program
147
+ .command("run")
148
+ .requiredOption("--input <string>", "Input for the agent")
149
+ .requiredOption("-d, --deployment <string>", "Specific deployment to run")
150
+ .action(async (options) => {
151
+ const apiKey = await requireAuth();
152
+ console.log("Running agent...");
153
+ const response = await fetch(`${API_BASE_URL}/run`, {
154
+ method: "POST",
155
+ headers: {
156
+ "Content-Type": "application/json",
157
+ "X-API-Key": `Bearer ${apiKey}`,
158
+ },
159
+ body: JSON.stringify({
160
+ input: options.input,
161
+ deploymentId: options.deployment,
162
+ }),
163
+ });
164
+ if (!response.ok) {
165
+ console.error("Running agent failed:", response.statusText);
166
+ process.exit(1);
167
+ }
168
+ });
169
+ const env = program.command("env").description("Manage environment variables");
170
+ env.command("set <key> <value>").action((key, value) => {
171
+ const config = readConfig();
172
+ config.envs = { ...config.envs, [key]: value };
173
+ writeConfig(config);
174
+ console.log(`✓ ${key} set`);
175
+ });
176
+ env.command("list").action(() => {
177
+ const config = readConfig();
178
+ console.log(config.envs ?? "No env keys configured");
179
+ });
180
+ env.command("delete <key>").action((key) => {
181
+ const config = readConfig();
182
+ delete config.envs?.[key];
183
+ writeConfig(config);
184
+ console.log(`✓ ${key} deleted`);
185
+ });
186
+ program.parse();
187
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { PassThrough } from "stream";
2
+ export declare function zipProject(): Promise<PassThrough>;
3
+ export declare function getAPIKey(): Promise<string | null>;
4
+ export declare function requireAuth(): Promise<string>;
5
+ export declare function authSuccessMessage(): Promise<void>;
6
+ export declare const API_BASE_URL: string;
7
+ export declare const WEB_BASE_URL: string;
8
+ export declare function readConfig(): any;
9
+ export declare function writeConfig(data: object): void;
10
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAMrC,wBAAsB,UAAU,yBAuB/B;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAWnD;AAED,wBAAsB,kBAAkB,kBAwBvC;AAED,eAAO,MAAM,YAAY,QAC6B,CAAC;AAEvD,eAAO,MAAM,YAAY,QAAmD,CAAC;AAI7E,wBAAgB,UAAU,QAGzB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,QAGvC"}
package/dist/utils.js ADDED
@@ -0,0 +1,73 @@
1
+ import archiver from "archiver";
2
+ import chalk from "chalk";
3
+ import { PassThrough } from "stream";
4
+ import keytar from "keytar";
5
+ import path from "path";
6
+ import os from "os";
7
+ import fs from "fs";
8
+ export async function zipProject() {
9
+ const archive = archiver("zip", { zlib: { level: 9 } });
10
+ const stream = new PassThrough();
11
+ archive.pipe(stream);
12
+ archive.glob("**/*", {
13
+ cwd: process.cwd(),
14
+ ignore: [
15
+ "**/node_modules/**",
16
+ "**/.git/**",
17
+ "**/.gitignore",
18
+ "**/.venv/**",
19
+ "**/__pycache__/**",
20
+ "**/.env",
21
+ "**/.env.**",
22
+ "**/dist/**",
23
+ ],
24
+ });
25
+ archive.finalize();
26
+ return stream;
27
+ }
28
+ export async function getAPIKey() {
29
+ const apiKey = await keytar.getPassword("ao-cli", "api-key");
30
+ return apiKey || null;
31
+ }
32
+ export async function requireAuth() {
33
+ const apiKey = await getAPIKey();
34
+ if (!apiKey) {
35
+ console.error(chalk.red("✗ You are not logged in. Please run 'ao login' first."));
36
+ process.exit(1);
37
+ }
38
+ return apiKey;
39
+ }
40
+ export async function authSuccessMessage() {
41
+ console.log("");
42
+ console.log(chalk.white(`
43
+ $@@@@$$ $@$@@@@@@@@@@@@$$$$
44
+ $@@@@@@@@$ $@@@@@@@@@@@@@@@@@@@@@$$
45
+ $@@@@@@@@@@@ $@@@@@@@$$$$$$$$$$@@$$ $$
46
+ $@@@@@$ @@@@@@$ $@@@@@@$ @@@@@@$
47
+ $@@@@@$ $@@@@@$ @@@@@$ $@@@@$
48
+ $@@@@@@$ $@@@@@$ @@@@@$ $@@@@@$
49
+ $@@@@@$ $@@@@@$ $@$$$ @@@@@$
50
+ $@@@@@@ $@@@@$ $@$ $@@@@@$
51
+ @@@@@@$ $@@@@$ $@@@@@
52
+ $@@@@@@$ $$@@@@@@@$@@@@@$$ $@@@@@$
53
+ $@@@@@$ $$$$$ $$@@@@@$@@@@@@@$@$$@@$$@@$$$@@@@@@@$
54
+ $@@@@@$ $$$$ $@@@@@$$@@@@@@@@@@@@@@@@@@@@@@@$
55
+ @@@@@@$ $$@@@@$ $$$@@@@@@@@@@@@@@@@$$
56
+ `));
57
+ console.log("");
58
+ console.log(chalk.green.bold("✔ Logged in successfully!"));
59
+ console.log("");
60
+ }
61
+ export const API_BASE_URL = process.env.AO_API_URL ?? "https://api.aodeploy.com";
62
+ export const WEB_BASE_URL = process.env.AO_WEB_URL ?? "https://aodeploy.com";
63
+ const CONFIG_PATH = path.join(os.homedir(), ".ao", "config.json");
64
+ export function readConfig() {
65
+ if (!fs.existsSync(CONFIG_PATH))
66
+ return {};
67
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
68
+ }
69
+ export function writeConfig(data) {
70
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
71
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2));
72
+ }
73
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@ao-ai/cli",
3
+ "version": "0.0.0",
4
+ "description": "AO CLI",
5
+ "license": "ISC",
6
+ "author": "Martin",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "bin": {
11
+ "ao": "dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "echo \"Error: no test specified\" && exit 1"
16
+ },
17
+ "dependencies": {
18
+ "archiver": "^4.0.2",
19
+ "chalk": "^5.6.2",
20
+ "commander": "^14.0.3",
21
+ "keytar": "^7.9.0",
22
+ "stream": "^0.0.3",
23
+ "toml": "^3.0.0",
24
+ "zod": "^4.3.6"
25
+ },
26
+ "devDependencies": {
27
+ "@types/archiver": "^7.0.0",
28
+ "@types/node": "^25.3.0",
29
+ "typescript": "^5.9.3"
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import path from "path";
5
+ import fs from "fs";
6
+ import open from "open";
7
+ import toml from "toml";
8
+ import { z } from "zod";
9
+ import http from "http";
10
+ import {
11
+ API_BASE_URL,
12
+ authSuccessMessage,
13
+ getAPIKey,
14
+ readConfig,
15
+ requireAuth,
16
+ WEB_BASE_URL,
17
+ writeConfig,
18
+ zipProject,
19
+ } from "./utils.js";
20
+ import { Readable } from "stream";
21
+ import chalk from "chalk";
22
+ import keytar from "keytar";
23
+
24
+ const program = new Command();
25
+
26
+ program.name("ao").description("AO CLI").version("0.0.1");
27
+
28
+ program
29
+ .command("init")
30
+ .action(() => {
31
+ const currentDir = process.cwd();
32
+
33
+ const filePath = path.join(currentDir, "ao.toml");
34
+
35
+ if (fs.existsSync(filePath)) {
36
+ console.log("AO configuration file already exists.");
37
+ return;
38
+ }
39
+
40
+ const initialContent = `
41
+ name = "my-project"
42
+
43
+ [runtime]
44
+ max_retries = 3
45
+ timeout = 300 # 5 minutes
46
+ `;
47
+
48
+ fs.writeFileSync(filePath, initialContent.trim());
49
+
50
+ console.log("Initialized AO configuration file at:", filePath);
51
+ })
52
+ .description("");
53
+
54
+ program.command("login").action(async () => {
55
+ const apiKey = await getAPIKey();
56
+
57
+ if (apiKey) {
58
+ console.log(chalk.green("You are already logged in."));
59
+ process.exit(0);
60
+ }
61
+
62
+ const server = http.createServer(async (req, res) => {
63
+ const address = server.address();
64
+ const port = typeof address === "object" && address ? address.port : 0;
65
+
66
+ const url = new URL(req.url!, `http://localhost:${port}`);
67
+ const apiKey = url.searchParams.get("apiKey");
68
+
69
+ if (apiKey) {
70
+ await keytar.setPassword("ao-cli", "api-key", apiKey);
71
+ res.writeHead(302, {
72
+ Location: `${WEB_BASE_URL}/auth/cli/success`,
73
+ });
74
+ res.end();
75
+
76
+ authSuccessMessage();
77
+ server.close();
78
+ process.exit(0);
79
+ }
80
+ });
81
+
82
+ await new Promise<void>((resolve) => {
83
+ server.listen(0, resolve);
84
+ });
85
+
86
+ const { port } = server.address() as any;
87
+
88
+ const authUrl = `${WEB_BASE_URL}/auth/cli/start?redirect=http://localhost:${port}`;
89
+ await open(authUrl);
90
+ });
91
+
92
+ program.command("logout").action(async () => {
93
+ await keytar.deletePassword("ao-cli", "api-key");
94
+
95
+ console.log(chalk.green("✓ Logged out successfully"));
96
+ });
97
+
98
+ program.command("deploy").action(async () => {
99
+ const apiKey = await requireAuth();
100
+
101
+ const currentDir = process.cwd();
102
+
103
+ const filePath = path.join(currentDir, "ao.toml");
104
+
105
+ const pyProjectFilePath = path.join(currentDir, "pyproject.toml");
106
+
107
+ const requirementsFilePath = path.join(currentDir, "requirements.txt");
108
+
109
+ if (!fs.existsSync(filePath)) {
110
+ console.error(
111
+ "AO configuration file not found. Please run 'ao init' first.",
112
+ );
113
+ return;
114
+ }
115
+
116
+ if (!fs.existsSync(requirementsFilePath)) {
117
+ console.error("requirements.txt is required");
118
+ return;
119
+ }
120
+
121
+ if (!fs.existsSync(pyProjectFilePath)) {
122
+ console.error(
123
+ "pyproject.toml file not found. Please make sure you have a pyproject.toml file in the current directory.",
124
+ );
125
+ return;
126
+ }
127
+
128
+ console.log("Deploying agent...");
129
+
130
+ // Get the configuration from ao.toml
131
+ const configContent = fs.readFileSync(filePath, "utf-8");
132
+
133
+ // Parse the TOML content and extract necessary information for deployment
134
+ let config;
135
+
136
+ try {
137
+ config = toml.parse(configContent);
138
+ } catch (error) {
139
+ console.error("Invalid TOML syntax in ao.toml:", error);
140
+ process.exit(1);
141
+ }
142
+
143
+ const configFileSchema = z.object({
144
+ name: z
145
+ .string()
146
+ .lowercase()
147
+ .min(3, "The 'name' field must be at least 3 characters long."),
148
+ deploy: z.object({
149
+ entrypoint: z.string(),
150
+ }),
151
+ runtime: z.object({
152
+ max_retries: z.number().int().nonnegative().optional().default(3),
153
+ timeout: z.number().int().positive().optional().default(300),
154
+ }),
155
+ schedule: z
156
+ .object({
157
+ cron: z.string(),
158
+ })
159
+ .optional(),
160
+ });
161
+
162
+ // Check if the AO config file structure is valid
163
+ const validation = configFileSchema.safeParse(config);
164
+
165
+ if (!validation.success) {
166
+ console.error("Invalid ao.toml structure:");
167
+ console.error(validation.error.message);
168
+ process.exit(1);
169
+ }
170
+
171
+ // Zip the agent code and dependencies
172
+ // 1. Node stream -> Web stream
173
+ console.log("Packing project...");
174
+
175
+ const zipNodeStream = await zipProject(); // Readable (Node)
176
+ const zipWebStream = Readable.toWeb(zipNodeStream);
177
+
178
+ // 2. Web stream -> Blob
179
+ const zipBlob = await new Response(zipWebStream).blob();
180
+
181
+ // Read env variables
182
+ const env = readConfig();
183
+
184
+ const form = new FormData();
185
+ form.append("file", zipBlob, "agent.zip");
186
+ form.append("envs", JSON.stringify(env.envs ?? {}));
187
+
188
+ const response = await fetch(`${API_BASE_URL}/deploy`, {
189
+ method: "POST",
190
+ headers: {
191
+ "X-API-Key": `Bearer ${apiKey}`,
192
+ },
193
+ body: form,
194
+ });
195
+
196
+ if (!response.ok) {
197
+ console.error("Deployment failed:", response.statusText);
198
+ process.exit(1);
199
+ }
200
+
201
+ console.log("Deployment successful!");
202
+ console.log(
203
+ "You can view your new deployment at:",
204
+ `https://aodeploy.com/dashboard/deployments`,
205
+ );
206
+ });
207
+
208
+ program
209
+ .command("run")
210
+ .requiredOption("--input <string>", "Input for the agent")
211
+ .requiredOption("-d, --deployment <string>", "Specific deployment to run")
212
+ .action(async (options) => {
213
+ const apiKey = await requireAuth();
214
+
215
+ console.log("Running agent...");
216
+ const response = await fetch(`${API_BASE_URL}/run`, {
217
+ method: "POST",
218
+ headers: {
219
+ "Content-Type": "application/json",
220
+ "X-API-Key": `Bearer ${apiKey}`,
221
+ },
222
+ body: JSON.stringify({
223
+ input: options.input,
224
+ deploymentId: options.deployment,
225
+ }),
226
+ });
227
+
228
+ if (!response.ok) {
229
+ console.error("Running agent failed:", response.statusText);
230
+ process.exit(1);
231
+ }
232
+ });
233
+
234
+ const env = program.command("env").description("Manage environment variables");
235
+
236
+ env.command("set <key> <value>").action((key, value) => {
237
+ const config = readConfig();
238
+ config.envs = { ...config.envs, [key]: value };
239
+ writeConfig(config);
240
+ console.log(`✓ ${key} set`);
241
+ });
242
+
243
+ env.command("list").action(() => {
244
+ const config = readConfig();
245
+ console.log(config.envs ?? "No env keys configured");
246
+ });
247
+
248
+ env.command("delete <key>").action((key) => {
249
+ const config = readConfig();
250
+ delete config.envs?.[key];
251
+ writeConfig(config);
252
+ console.log(`✓ ${key} deleted`);
253
+ });
254
+
255
+ program.parse();
package/src/utils.ts ADDED
@@ -0,0 +1,94 @@
1
+ import archiver from "archiver";
2
+ import chalk from "chalk";
3
+ import { PassThrough } from "stream";
4
+ import keytar from "keytar";
5
+ import path from "path";
6
+ import os from "os";
7
+ import fs from "fs";
8
+
9
+ export async function zipProject() {
10
+ const archive = archiver("zip", { zlib: { level: 9 } });
11
+ const stream = new PassThrough();
12
+
13
+ archive.pipe(stream);
14
+
15
+ archive.glob("**/*", {
16
+ cwd: process.cwd(),
17
+ ignore: [
18
+ "**/node_modules/**",
19
+ "**/.git/**",
20
+ "**/.gitignore",
21
+ "**/.venv/**",
22
+ "**/__pycache__/**",
23
+ "**/.env",
24
+ "**/.env.**",
25
+ "**/dist/**",
26
+ ],
27
+ });
28
+
29
+ archive.finalize();
30
+
31
+ return stream;
32
+ }
33
+
34
+ export async function getAPIKey(): Promise<string | null> {
35
+ const apiKey = await keytar.getPassword("ao-cli", "api-key");
36
+
37
+ return apiKey || null;
38
+ }
39
+
40
+ export async function requireAuth(): Promise<string> {
41
+ const apiKey = await getAPIKey();
42
+
43
+ if (!apiKey) {
44
+ console.error(
45
+ chalk.red("✗ You are not logged in. Please run 'ao login' first."),
46
+ );
47
+ process.exit(1);
48
+ }
49
+
50
+ return apiKey;
51
+ }
52
+
53
+ export async function authSuccessMessage() {
54
+ console.log("");
55
+
56
+ console.log(
57
+ chalk.white(`
58
+ $@@@@$$ $@$@@@@@@@@@@@@$$$$
59
+ $@@@@@@@@$ $@@@@@@@@@@@@@@@@@@@@@$$
60
+ $@@@@@@@@@@@ $@@@@@@@$$$$$$$$$$@@$$ $$
61
+ $@@@@@$ @@@@@@$ $@@@@@@$ @@@@@@$
62
+ $@@@@@$ $@@@@@$ @@@@@$ $@@@@$
63
+ $@@@@@@$ $@@@@@$ @@@@@$ $@@@@@$
64
+ $@@@@@$ $@@@@@$ $@$$$ @@@@@$
65
+ $@@@@@@ $@@@@$ $@$ $@@@@@$
66
+ @@@@@@$ $@@@@$ $@@@@@
67
+ $@@@@@@$ $$@@@@@@@$@@@@@$$ $@@@@@$
68
+ $@@@@@$ $$$$$ $$@@@@@$@@@@@@@$@$$@@$$@@$$$@@@@@@@$
69
+ $@@@@@$ $$$$ $@@@@@$$@@@@@@@@@@@@@@@@@@@@@@@$
70
+ @@@@@@$ $$@@@@$ $$$@@@@@@@@@@@@@@@@$$
71
+ `),
72
+ );
73
+
74
+ console.log("");
75
+ console.log(chalk.green.bold("✔ Logged in successfully!"));
76
+ console.log("");
77
+ }
78
+
79
+ export const API_BASE_URL =
80
+ process.env.AO_API_URL ?? "https://api.aodeploy.com";
81
+
82
+ export const WEB_BASE_URL = process.env.AO_WEB_URL ?? "https://aodeploy.com";
83
+
84
+ const CONFIG_PATH = path.join(os.homedir(), ".ao", "config.json");
85
+
86
+ export function readConfig() {
87
+ if (!fs.existsSync(CONFIG_PATH)) return {};
88
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
89
+ }
90
+
91
+ export function writeConfig(data: object) {
92
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
93
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2));
94
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "nodenext",
11
+ "target": "esnext",
12
+ // For nodejs:
13
+ "lib": ["esnext"],
14
+ "types": ["node"],
15
+
16
+ // Other Outputs
17
+ "sourceMap": true,
18
+ "declaration": true,
19
+ "declarationMap": true,
20
+
21
+ // Stricter Typechecking Options
22
+ "noUncheckedIndexedAccess": true,
23
+ "exactOptionalPropertyTypes": true,
24
+
25
+ // Style Options
26
+ // "noImplicitReturns": true,
27
+ // "noImplicitOverride": true,
28
+ // "noUnusedLocals": true,
29
+ // "noUnusedParameters": true,
30
+ // "noFallthroughCasesInSwitch": true,
31
+ // "noPropertyAccessFromIndexSignature": true,
32
+
33
+ // Recommended Options
34
+ "strict": true,
35
+ "jsx": "react-jsx",
36
+ "verbatimModuleSyntax": true,
37
+ "isolatedModules": true,
38
+ "noUncheckedSideEffectImports": true,
39
+ "moduleDetection": "force",
40
+ "skipLibCheck": true,
41
+ }
42
+ }