@zuplo/cli 1.50.0 → 1.52.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/dist/cli.js CHANGED
@@ -7,7 +7,9 @@ import convert from "./cmds/convert.js";
7
7
  import deleteZup from "./cmds/delete.js";
8
8
  import deploy from "./cmds/deploy.js";
9
9
  import dev from "./cmds/dev.js";
10
+ import link from "./cmds/link.js";
10
11
  import list from "./cmds/list.js";
12
+ import login from "./cmds/login.js";
11
13
  import test from "./cmds/test.js";
12
14
  import tunnel from "./cmds/tunnel/index.js";
13
15
  import variable from "./cmds/variable/index.js";
@@ -21,6 +23,8 @@ if (gte(process.versions.node, MIN_NODE_VERSION)) {
21
23
  .command(deploy)
22
24
  .command(dev)
23
25
  .command(list)
26
+ .command(link)
27
+ .command(login)
24
28
  .command(test)
25
29
  .command(tunnel)
26
30
  .command(variable)
@@ -0,0 +1,26 @@
1
+ import setBlocking from "../common/output.js";
2
+ import { validLinkDirectoryValidator } from "../common/validators/file-system-validator.js";
3
+ import { YargsChecker } from "../common/validators/lib.js";
4
+ import { link } from "../link/handler.js";
5
+ export default {
6
+ desc: "Link a local directory to a Zuplo project",
7
+ command: "link",
8
+ builder: (yargs) => {
9
+ return yargs
10
+ .option("dir", {
11
+ type: "string",
12
+ describe: "The directory containing your zup",
13
+ default: ".",
14
+ normalize: true,
15
+ hidden: true,
16
+ })
17
+ .middleware([setBlocking])
18
+ .check(async (argv) => {
19
+ return await new YargsChecker(validLinkDirectoryValidator).check(argv);
20
+ });
21
+ },
22
+ handler: async (argv) => {
23
+ await link(argv);
24
+ },
25
+ };
26
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1,18 @@
1
+ import setBlocking from "../common/output.js";
2
+ import { login } from "../login/handler.js";
3
+ export default {
4
+ desc: "Authenticates the user",
5
+ command: "login",
6
+ builder: (yargs) => {
7
+ return yargs
8
+ .option("print-token", {
9
+ type: "boolean",
10
+ describe: "Should the CLI print the token to the console?",
11
+ })
12
+ .middleware([setBlocking]);
13
+ },
14
+ handler: async (argv) => {
15
+ await login(argv);
16
+ },
17
+ };
18
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lib.js.map
@@ -1,5 +1,8 @@
1
1
  export const ZUPLO_PREFERRED_JSON_FILE = "zuplo.jsonc";
2
2
  export const ZUPLO_FALLBACK_JSON_FILE = "zuplo.json";
3
+ export const ZUPLO_CLI_XDG_FOLDER_NAME = "zup";
4
+ export const ZUPLO_AUTH_FILE_NAME = "auth.json";
5
+ export const ZUPLO_SYSTEM_ENV_VAR = ".env.zuplo";
3
6
  export const DEPLOYER_METADATA_FILE = "deployer.json";
4
7
  export const TEST_IN_FOLDER = "tests";
5
8
  export const TEST_OUT_FOLDER = ".zuplo/__tests__";
@@ -2,6 +2,9 @@ class Settings {
2
2
  get ZUPLO_DEVELOPER_API_ENDPOINT() {
3
3
  return process.env.ZUPLO_DEVELOPER_API_ENDPOINT ?? "https://dev.zuplo.com";
4
4
  }
5
+ get ZUPLO_API_ENDPOINT() {
6
+ return process.env.ZUPLO_API_ENDPOINT ?? "https://api.zuplo.com";
7
+ }
5
8
  get MAX_POLL_RETRIES() {
6
9
  return parseInt(process.env.MAX_POLL_RETRIES ?? "90");
7
10
  }
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { simpleGit } from "simple-git";
5
5
  import { TEST_IN_FOLDER } from "../constants.js";
6
6
  import { CompositeValidator } from "./lib.js";
7
+ import { UserIsLoggedInValidator } from "./login-state-validator.js";
7
8
  import { ProjectIsSetValidator } from "./project-name-validator.js";
8
9
  export class NotAGitRepoError extends Error {
9
10
  constructor() {
@@ -158,4 +159,5 @@ export class ZuploProjectHasTestsValidator {
158
159
  }
159
160
  export const validDeployDirectoryValidator = new CompositeValidator(new ZuploProjectValidator(), new GitVersionControlValidator(), new GitCommitValidator(), new GitBranchValidator(), new GitRemoteValidator(), new ProjectIsSetValidator());
160
161
  export const validTestDirectoryValidator = new CompositeValidator(new ZuploProjectValidator(), new ZuploProjectHasTestsValidator());
162
+ export const validLinkDirectoryValidator = new CompositeValidator(new ZuploProjectValidator(), new UserIsLoggedInValidator());
161
163
  //# sourceMappingURL=file-system-validator.js.map
@@ -0,0 +1,36 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { ZUPLO_AUTH_FILE_NAME } from "../constants.js";
5
+ import { logger } from "../logger.js";
6
+ import settings from "../settings.js";
7
+ import { ZUPLO_XDG_STATE_HOME } from "../xdg/lib.js";
8
+ export class UserIsNotLoggedIn extends Error {
9
+ constructor() {
10
+ super(`You are not logged in (or your credentials have expired). Please run \`zuplo login\` to log in.`);
11
+ Object.setPrototypeOf(this, UserIsNotLoggedIn.prototype);
12
+ }
13
+ }
14
+ export class UserIsLoggedInValidator {
15
+ async validate(options) {
16
+ if (!existsSync(join(ZUPLO_XDG_STATE_HOME, ZUPLO_AUTH_FILE_NAME))) {
17
+ return { ok: false, error: new UserIsNotLoggedIn() };
18
+ }
19
+ const rawAuth = await readFile(join(ZUPLO_XDG_STATE_HOME, ZUPLO_AUTH_FILE_NAME), "utf-8");
20
+ const authJson = JSON.parse(rawAuth);
21
+ const accountResponse = await fetch(`${settings.ZUPLO_API_ENDPOINT}/v1/accounts`, {
22
+ headers: {
23
+ authorization: `Bearer ${authJson.access_token}`,
24
+ },
25
+ });
26
+ if (!accountResponse.ok) {
27
+ logger.trace({
28
+ status: accountResponse.status,
29
+ statusText: accountResponse.statusText,
30
+ }, "Failed to connect to Zuplo API during verification. Assuming user is not logged in.");
31
+ return { ok: false, error: new UserIsNotLoggedIn() };
32
+ }
33
+ return { ok: true };
34
+ }
35
+ }
36
+ //# sourceMappingURL=login-state-validator.js.map
@@ -0,0 +1,18 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ import { ZUPLO_CLI_XDG_FOLDER_NAME } from "../constants.js";
4
+ function defineDirectoryWithFallback(xdgName, fallback) {
5
+ if (process.env[xdgName]) {
6
+ return process.env[xdgName];
7
+ }
8
+ else {
9
+ return path.join(homedir(), fallback);
10
+ }
11
+ }
12
+ export const XDG_CONFIG_HOME = defineDirectoryWithFallback("XDG_CONFIG_HOME", ".config");
13
+ export const XDG_DATA_HOME = defineDirectoryWithFallback("XDG_DATA_HOME", ".local/share");
14
+ export const XDG_STATE_HOME = defineDirectoryWithFallback("XDG_DATA_HOME", ".local/state");
15
+ export const ZUPLO_XDG_CONFIG_HOME = path.join(XDG_CONFIG_HOME, ZUPLO_CLI_XDG_FOLDER_NAME);
16
+ export const ZUPLO_XDG_DATA_HOME = path.join(XDG_DATA_HOME, ZUPLO_CLI_XDG_FOLDER_NAME);
17
+ export const ZUPLO_XDG_STATE_HOME = path.join(XDG_STATE_HOME, ZUPLO_CLI_XDG_FOLDER_NAME);
18
+ //# sourceMappingURL=lib.js.map
@@ -1,4 +1,6 @@
1
+ import * as dotenv from "dotenv";
1
2
  import { cpSync, existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
2
4
  import { join, relative, resolve } from "node:path";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import { locateDenoExecutable } from "../common/deno-utils/locator.js";
@@ -12,6 +14,11 @@ export async function dev(argv) {
12
14
  }
13
15
  process.env.GLOBAL_MODULE_LOCATION = fileURLToPath(new URL("../../node_modules", import.meta.url));
14
16
  process.env.DENO_EXECUTABLE = await locateDenoExecutable();
17
+ const envFile = join(sourceDirectory, ".env.zuplo");
18
+ if (existsSync(envFile)) {
19
+ const config = dotenv.parse(await readFile(envFile, "utf-8"));
20
+ dotenv.populate(process.env, config);
21
+ }
15
22
  const core = await import("@zuplo/core");
16
23
  const port = argv.port ?? 9000;
17
24
  await core.default.startDevServer({
@@ -0,0 +1,77 @@
1
+ import { select } from "@inquirer/prompts";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { ZUPLO_AUTH_FILE_NAME } from "../common/constants.js";
5
+ import { logger } from "../common/logger.js";
6
+ import { printDiagnosticsToConsole } from "../common/output.js";
7
+ import settings from "../common/settings.js";
8
+ import { ZUPLO_XDG_STATE_HOME } from "../common/xdg/lib.js";
9
+ import { pullSystemConfig, safeMergeConfig } from "./populate.js";
10
+ export async function link(argv) {
11
+ const rawAuth = await readFile(join(ZUPLO_XDG_STATE_HOME, ZUPLO_AUTH_FILE_NAME), "utf-8");
12
+ const authJson = JSON.parse(rawAuth);
13
+ const accountResponse = await fetch(`${settings.ZUPLO_API_ENDPOINT}/v1/accounts`, {
14
+ headers: {
15
+ authorization: `Bearer ${authJson.access_token}`,
16
+ },
17
+ });
18
+ if (!accountResponse.ok) {
19
+ logger.error({
20
+ status: accountResponse.status,
21
+ statusText: accountResponse.statusText,
22
+ response: await accountResponse.text(),
23
+ }, "Failed to list accounts.");
24
+ printDiagnosticsToConsole("Error: Failed to list your accounts. Try again later.");
25
+ }
26
+ const accountJson = (await accountResponse.json());
27
+ let account;
28
+ if (accountJson.length === 1) {
29
+ account = accountJson[0].name;
30
+ }
31
+ else {
32
+ account = await select({
33
+ message: "Select the account to work with",
34
+ choices: accountJson.map((acc) => {
35
+ return {
36
+ name: acc.name,
37
+ value: acc.name,
38
+ };
39
+ }),
40
+ });
41
+ }
42
+ const projectResponse = await fetch(`${settings.ZUPLO_API_ENDPOINT}/v1/accounts/${account}/projects`, {
43
+ headers: {
44
+ authorization: `Bearer ${authJson.access_token}`,
45
+ },
46
+ });
47
+ if (!projectResponse.ok) {
48
+ logger.error({
49
+ status: projectResponse.status,
50
+ statusText: projectResponse.statusText,
51
+ response: await projectResponse.text(),
52
+ }, "Failed to list projects.");
53
+ printDiagnosticsToConsole("Error: Failed to list your projects. Try again later.");
54
+ }
55
+ const projectJson = (await projectResponse.json());
56
+ let project;
57
+ if (projectJson.data.length === 1) {
58
+ project = projectJson.data[0].name;
59
+ }
60
+ else {
61
+ project = await select({
62
+ message: "Select the project to work with",
63
+ choices: projectJson.data.map((prj) => {
64
+ return {
65
+ name: prj.name,
66
+ value: prj.name,
67
+ };
68
+ }),
69
+ });
70
+ }
71
+ await safeMergeConfig(argv.dir, { account, project });
72
+ await pullSystemConfig(argv.dir, {
73
+ account: accountJson.find((acc) => acc.name === account),
74
+ project: projectJson.data.find((prj) => prj.name === project),
75
+ });
76
+ }
77
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1,65 @@
1
+ import { applyEdits, modify } from "jsonc-parser";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { join, relative } from "node:path";
5
+ import prettier from "prettier";
6
+ import { ZUPLO_FALLBACK_JSON_FILE, ZUPLO_PREFERRED_JSON_FILE, ZUPLO_SYSTEM_ENV_VAR, } from "../common/constants.js";
7
+ export async function safeMergeConfig(dir, options) {
8
+ const normalizedDir = join(relative(process.cwd(), dir));
9
+ const zuploPreferredConfigFile = join(normalizedDir, ZUPLO_PREFERRED_JSON_FILE);
10
+ const zuploFallbackConfigFile = join(normalizedDir, ZUPLO_FALLBACK_JSON_FILE);
11
+ if (existsSync(zuploPreferredConfigFile)) {
12
+ const originalContents = await readFile(zuploPreferredConfigFile, "utf-8");
13
+ const modifyAccountEdit = modify(originalContents, ["account"], options.account, {
14
+ getInsertionIndex: () => 1,
15
+ });
16
+ const contentsPostAccount = applyEdits(originalContents, modifyAccountEdit);
17
+ const modifyProjectEdit = modify(contentsPostAccount, ["project"], options.project, {
18
+ getInsertionIndex: () => 2,
19
+ });
20
+ const contentsPostProject = applyEdits(contentsPostAccount, modifyProjectEdit);
21
+ const formatted = prettier.format(contentsPostProject, {
22
+ parser: "json",
23
+ quoteProps: "preserve",
24
+ });
25
+ await writeFile(zuploPreferredConfigFile, formatted);
26
+ }
27
+ else if (existsSync(zuploFallbackConfigFile)) {
28
+ const config = JSON.parse(await readFile(zuploFallbackConfigFile, "utf-8"));
29
+ const formatted = prettier.format(JSON.stringify({
30
+ version: 1,
31
+ account: options.account,
32
+ project: options.project,
33
+ compatibilityDate: config.compatibilityDate,
34
+ }), {
35
+ parser: "json",
36
+ quoteProps: "preserve",
37
+ });
38
+ await writeFile(zuploPreferredConfigFile, formatted);
39
+ }
40
+ else {
41
+ const formatted = prettier.format(JSON.stringify({
42
+ version: 1,
43
+ account: options.account,
44
+ project: options.project,
45
+ compatibilityDate: "2023-03-14",
46
+ }), {
47
+ parser: "json",
48
+ quoteProps: "preserve",
49
+ });
50
+ await writeFile(zuploPreferredConfigFile, formatted);
51
+ }
52
+ }
53
+ export async function pullSystemConfig(dir, options) {
54
+ const normalizedDir = join(relative(process.cwd(), dir));
55
+ const zuploPreferredConfigFile = join(normalizedDir, ZUPLO_SYSTEM_ENV_VAR);
56
+ const content = `
57
+ # This file is auto-generated from zup link. Please do not edit it manually.
58
+ # It will be auto-generated afresh the next time you run zup link.
59
+
60
+ ZUPLO_API_KEY_SERVICE_BUCKET_NAME=z${options.project.id
61
+ .toLowerCase()
62
+ .replaceAll("_", "-")}-working-copy`;
63
+ await writeFile(zuploPreferredConfigFile, content);
64
+ }
65
+ //# sourceMappingURL=populate.js.map
@@ -0,0 +1,75 @@
1
+ import crypto from "node:crypto";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { ZUPLO_AUTH_FILE_NAME } from "../common/constants.js";
6
+ import { printResultToConsoleAndExitGracefully } from "../common/output.js";
7
+ import { ZUPLO_XDG_STATE_HOME } from "../common/xdg/lib.js";
8
+ import { browserAuth } from "./server.js";
9
+ export const AUTH_SERVER_PORT = 57801;
10
+ export const CLIENT_ID = "mYLGcH7kB4P0pw0HAk6GH7raRwYhSlW4";
11
+ export const CALLBACK_URL = `http://localhost:${AUTH_SERVER_PORT}/`;
12
+ export const AUTH0_DOMAIN = "auth.zuplo.com";
13
+ export const encode = (value) => btoa(value);
14
+ export const decode = (value) => atob(value);
15
+ export const createRandomString = () => {
16
+ const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.";
17
+ let random = "";
18
+ const randomValues = Array.from(crypto.getRandomValues(new Uint8Array(43)));
19
+ randomValues.forEach((v) => (random += charset[v % charset.length]));
20
+ return random;
21
+ };
22
+ const urlEncodeB64 = (input) => {
23
+ const b64Chars = { "+": "-", "/": "_", "=": "" };
24
+ return input.replace(/[+/=]/g, (m) => b64Chars[m]);
25
+ };
26
+ export const bufferToBase64UrlEncoded = (input) => {
27
+ const ie11SafeInput = new Uint8Array(input);
28
+ return urlEncodeB64(btoa(String.fromCharCode(...Array.from(ie11SafeInput))));
29
+ };
30
+ export const sha256 = async (s) => {
31
+ const digestOp = crypto.subtle.digest({ name: "SHA-256" }, new TextEncoder().encode(s));
32
+ return await digestOp;
33
+ };
34
+ export async function login(args) {
35
+ const code_verifier = createRandomString();
36
+ const code_challengeBuffer = await sha256(code_verifier);
37
+ const code_challenge = bufferToBase64UrlEncoded(code_challengeBuffer);
38
+ const authUrl = new URL(`https://${AUTH0_DOMAIN}/authorize`);
39
+ authUrl.searchParams.set("response_type", "code");
40
+ authUrl.searchParams.set("code_challenge", code_challenge);
41
+ authUrl.searchParams.set("code_challenge_method", "S256");
42
+ authUrl.searchParams.set("client_id", CLIENT_ID);
43
+ authUrl.searchParams.set("redirect_uri", CALLBACK_URL);
44
+ authUrl.searchParams.set("scope", "openid profile email");
45
+ authUrl.searchParams.set("audience", "https://api.zuplo.com/");
46
+ const params = await browserAuth(authUrl.toString());
47
+ const code = params.get("code");
48
+ if (code === null) {
49
+ throw new Error("No code");
50
+ }
51
+ const tokenParams = new URLSearchParams();
52
+ tokenParams.set("grant_type", "authorization_code");
53
+ tokenParams.set("client_id", CLIENT_ID);
54
+ tokenParams.set("code_verifier", code_verifier);
55
+ tokenParams.set("code", code);
56
+ tokenParams.set("redirect_uri", CALLBACK_URL);
57
+ const response = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
58
+ method: "POST",
59
+ headers: {
60
+ "Content-Type": "application/x-www-form-urlencoded",
61
+ },
62
+ body: tokenParams,
63
+ });
64
+ const result = (await response.json());
65
+ if (!existsSync(ZUPLO_XDG_STATE_HOME)) {
66
+ mkdirSync(ZUPLO_XDG_STATE_HOME, { recursive: true });
67
+ }
68
+ const tokenPath = join(ZUPLO_XDG_STATE_HOME, ZUPLO_AUTH_FILE_NAME);
69
+ await writeFile(tokenPath, JSON.stringify(result));
70
+ if (args["print-token"]) {
71
+ console.log(result.access_token);
72
+ }
73
+ printResultToConsoleAndExitGracefully("Successfully authenticated.");
74
+ }
75
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1,35 @@
1
+ import http from "http";
2
+ import opn from "open";
3
+ import { printCriticalFailureToConsoleAndExit } from "../common/output.js";
4
+ import { AUTH_SERVER_PORT } from "./handler.js";
5
+ export async function browserAuth(authorizationUrl) {
6
+ let params;
7
+ const handler = (req, res) => {
8
+ if (!req.url) {
9
+ throw new Error("Bad url");
10
+ }
11
+ const url = new URL(req.url, `http://localhost:${AUTH_SERVER_PORT}`);
12
+ if (url.pathname === "/") {
13
+ params = url.searchParams;
14
+ res.end("You can close this browser now.");
15
+ }
16
+ else {
17
+ res.end("Unsupported route for login server.");
18
+ }
19
+ };
20
+ const server = http.createServer(handler).listen(AUTH_SERVER_PORT);
21
+ opn(authorizationUrl);
22
+ let iterations = 0;
23
+ while (params === undefined) {
24
+ if (iterations++ > 60) {
25
+ server.close();
26
+ printCriticalFailureToConsoleAndExit("Timed out waiting for login in the browser. Run the zup login command again.");
27
+ }
28
+ else {
29
+ await new Promise((resolve) => setTimeout(resolve, 500));
30
+ }
31
+ }
32
+ server.close();
33
+ return params;
34
+ }
35
+ //# sourceMappingURL=server.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/cli",
3
- "version": "1.50.0",
3
+ "version": "1.52.0",
4
4
  "type": "module",
5
5
  "repository": "https://github.com/zuplo/cli",
6
6
  "author": "Zuplo, Inc.",
@@ -8,8 +8,7 @@
8
8
  "scripts": {
9
9
  "build": "tsc --build",
10
10
  "test": "mocha",
11
- "test:debug": "mocha --timeout 0",
12
- "prepack": "scripts/prepack.mjs"
11
+ "test:debug": "mocha --timeout 0"
13
12
  },
14
13
  "engines": {
15
14
  "node": ">=18.0.0"
@@ -50,18 +49,20 @@
50
49
  "typescript": "5.0.3"
51
50
  },
52
51
  "dependencies": {
52
+ "@inquirer/prompts": "^3.0.4",
53
53
  "@swc/core": "1.3.78",
54
- "@zuplo/core": "5.1206.0",
55
- "@zuplo/runtime": "5.1206.0",
54
+ "@zuplo/core": "5.1217.0",
55
+ "@zuplo/runtime": "5.1217.0",
56
56
  "chalk": "^5.1.2",
57
57
  "deno-bin": "1.31.1",
58
- "dotenv": "^16.0.3",
58
+ "dotenv": "^16.3.1",
59
59
  "esbuild": "0.18.6",
60
60
  "execa": "^6.1.0",
61
61
  "fast-glob": "^3.2.12",
62
62
  "ignore": "^5.2.4",
63
- "jsonc-parser": "^3.2.0",
64
63
  "jose": "^4.14.4",
64
+ "jsonc-parser": "^3.2.0",
65
+ "open": "^9.1.0",
65
66
  "pino": "^8.11.0",
66
67
  "pino-pretty": "^9.4.0",
67
68
  "prettier": "^2.8.7",