@saws/cli 2.0.0-beta.2 → 2.0.0-beta.20
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/README.md +206 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/saws.d.ts +0 -1
- package/dist/bin/saws.js +32 -40
- package/dist/commands/app/deploy.d.ts +4 -0
- package/dist/commands/app/deploy.js +20 -0
- package/dist/commands/app/files.d.ts +4 -0
- package/dist/commands/app/files.js +40 -0
- package/dist/commands/app/index.d.ts +2 -0
- package/dist/commands/app/index.js +22 -0
- package/dist/commands/app/install.d.ts +4 -0
- package/dist/commands/app/install.js +35 -0
- package/dist/commands/app/list.d.ts +1 -0
- package/dist/commands/app/list.js +67 -0
- package/dist/commands/app/metadata.d.ts +2 -0
- package/dist/commands/app/metadata.js +27 -0
- package/dist/commands/app/paths.d.ts +2 -0
- package/dist/commands/app/paths.js +11 -0
- package/dist/commands/app/process.d.ts +3 -0
- package/dist/commands/app/process.js +53 -0
- package/dist/commands/app/update.d.ts +1 -0
- package/dist/commands/app/update.js +62 -0
- package/dist/commands/deploy/command.d.ts +2 -7
- package/dist/commands/deploy/command.js +13 -19
- package/dist/commands/deploy/index.d.ts +0 -1
- package/dist/commands/deploy/index.js +1 -4
- package/dist/commands/dev/command.d.ts +1 -7
- package/dist/commands/dev/command.js +46 -48
- package/dist/commands/dev/index.d.ts +0 -1
- package/dist/commands/dev/index.js +1 -6
- package/dist/commands/dev/tui/dev-tui.d.ts +18 -3
- package/dist/commands/dev/tui/dev-tui.js +263 -29
- package/dist/commands/host/command.d.ts +3 -2
- package/dist/commands/host/command.js +112 -7
- package/dist/commands/host/create.d.ts +9 -0
- package/dist/commands/host/create.js +30 -0
- package/dist/commands/host/deployment-key.d.ts +2 -0
- package/dist/commands/host/deployment-key.js +40 -0
- package/dist/commands/host/import-key.d.ts +5 -0
- package/dist/commands/host/import-key.js +40 -0
- package/dist/commands/host/index.d.ts +0 -1
- package/dist/commands/host/index.js +25 -3
- package/dist/commands/init/command.d.ts +1 -7
- package/dist/commands/init/command.js +24 -15
- package/dist/commands/init/index.d.ts +0 -1
- package/dist/commands/init/index.js +1 -5
- package/dist/commands/init/templates/gitignore.template.d.ts +1 -0
- package/dist/commands/init/templates/gitignore.template.js +4 -0
- package/dist/commands/init/templates/saws-ts.template.d.ts +3 -0
- package/dist/commands/init/templates/saws-ts.template.js +7 -0
- package/dist/commands/init/templates/tsconfig-json.template.d.ts +1 -0
- package/dist/commands/init/templates/tsconfig-json.template.js +4 -0
- package/dist/commands/logs/command.d.ts +5 -0
- package/dist/commands/logs/command.js +19 -0
- package/dist/commands/logs/index.d.ts +2 -0
- package/dist/commands/logs/index.js +7 -0
- package/dist/commands/secrets/command.d.ts +2 -2
- package/dist/commands/secrets/command.js +21 -9
- package/dist/commands/secrets/index.d.ts +0 -1
- package/dist/commands/secrets/index.js +7 -5
- package/dist/hosts.d.ts +1 -3
- package/dist/hosts.js +11 -10
- package/docs/global-hosts-and-applications.md +307 -0
- package/package.json +13 -20
- package/dist/bin/saws.d.ts.map +0 -1
- package/dist/commands/deploy/command.d.ts.map +0 -1
- package/dist/commands/deploy/index.d.ts.map +0 -1
- package/dist/commands/dev/command.d.ts.map +0 -1
- package/dist/commands/dev/index.d.ts.map +0 -1
- package/dist/commands/dev/tui/dev-tui.d.ts.map +0 -1
- package/dist/commands/host/command.d.ts.map +0 -1
- package/dist/commands/host/command.test.d.ts +0 -2
- package/dist/commands/host/command.test.d.ts.map +0 -1
- package/dist/commands/host/command.test.js +0 -13
- package/dist/commands/host/index.d.ts.map +0 -1
- package/dist/commands/init/command.d.ts.map +0 -1
- package/dist/commands/init/command.test.d.ts +0 -2
- package/dist/commands/init/command.test.d.ts.map +0 -1
- package/dist/commands/init/command.test.js +0 -40
- package/dist/commands/init/index.d.ts.map +0 -1
- package/dist/commands/secrets/command.d.ts.map +0 -1
- package/dist/commands/secrets/index.d.ts.map +0 -1
- package/dist/hosts.d.ts.map +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function getPackageName(specifier) {
|
|
2
|
+
const trimmed = specifier.trim();
|
|
3
|
+
const match = /^(?<name>@[^/\s]+\/[^@/\s]+|[^@/\s]+)(?:@[^\s]+)?$/.exec(trimmed);
|
|
4
|
+
const name = match?.groups?.name;
|
|
5
|
+
if (name == null) {
|
|
6
|
+
throw new Error("Application package must be an npm package name with an optional version or tag");
|
|
7
|
+
}
|
|
8
|
+
return name;
|
|
9
|
+
}
|
|
10
|
+
export function applicationWrapper(packageName) {
|
|
11
|
+
return `import { createSawsConfig } from "@saws/core";
|
|
12
|
+
import config from "./config.ts";
|
|
13
|
+
import * as application from ${JSON.stringify(packageName)};
|
|
14
|
+
|
|
15
|
+
export * from "./config.ts";
|
|
16
|
+
|
|
17
|
+
export default await createSawsConfig(application, config);
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
export function applicationConfig(packageName) {
|
|
21
|
+
return `import type { create } from ${JSON.stringify(packageName)};
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
// Configure this application instance here.
|
|
25
|
+
} satisfies Parameters<typeof create>[0];
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
export async function pathExists(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(filePath);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === "ENOENT") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
import fs from "node:fs/promises";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { deployAppCommand } from "./deploy.js";
|
|
3
|
+
import { installAppCommand } from "./install.js";
|
|
4
|
+
import { listAppsCommand } from "./list.js";
|
|
5
|
+
import { updateAppCommand } from "./update.js";
|
|
6
|
+
export const createCommand = () => new Command("app")
|
|
7
|
+
.description("manage packaged SAWS applications")
|
|
8
|
+
.addCommand(new Command("list").description("list installed SAWS applications").action(listAppsCommand))
|
|
9
|
+
.addCommand(new Command("install")
|
|
10
|
+
.description("install a packaged SAWS application")
|
|
11
|
+
.argument("<package>", "npm package name with an optional version or tag")
|
|
12
|
+
.requiredOption("--name <name>", "local name for this application instance")
|
|
13
|
+
.action(installAppCommand))
|
|
14
|
+
.addCommand(new Command("update")
|
|
15
|
+
.description("update an installed SAWS application to npm latest")
|
|
16
|
+
.argument("<name>", "installed application name")
|
|
17
|
+
.action(updateAppCommand))
|
|
18
|
+
.addCommand(new Command("deploy")
|
|
19
|
+
.description("deploy an installed SAWS application")
|
|
20
|
+
.argument("<name>", "installed application name")
|
|
21
|
+
.requiredOption("--stage <stage>", "stage to deploy")
|
|
22
|
+
.action(deployAppCommand));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { applicationConfig, applicationWrapper, getPackageName } from "./files.js";
|
|
4
|
+
import { getAppDirectory } from "./paths.js";
|
|
5
|
+
import { runProcess } from "./process.js";
|
|
6
|
+
export async function installAppCommand(packageSpecifier, options) {
|
|
7
|
+
const packageName = getPackageName(packageSpecifier);
|
|
8
|
+
const appDirectory = getAppDirectory(options.name);
|
|
9
|
+
await fs.mkdir(path.dirname(appDirectory), { recursive: true });
|
|
10
|
+
try {
|
|
11
|
+
await fs.mkdir(appDirectory);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === "EEXIST") {
|
|
15
|
+
throw new Error(`Application "${options.name}" is already installed`);
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
await fs.writeFile(path.join(appDirectory, "package.json"), `${JSON.stringify({
|
|
21
|
+
name: "saws-installed-application",
|
|
22
|
+
private: true,
|
|
23
|
+
type: "module",
|
|
24
|
+
}, null, 2)}\n`);
|
|
25
|
+
await fs.writeFile(path.join(appDirectory, "config.ts"), applicationConfig(packageName));
|
|
26
|
+
await fs.writeFile(path.join(appDirectory, "saws.ts"), applicationWrapper(packageName));
|
|
27
|
+
await runProcess("npm", ["install", "--save-exact", "--no-fund", "--no-audit", packageSpecifier], appDirectory);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
await fs.rm(appDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
console.log(`Installed ${packageSpecifier} as application "${options.name}"`);
|
|
34
|
+
console.log(`Configure it by editing ${path.join(appDirectory, "config.ts")}`);
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function listAppsCommand(): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathExists } from "./files.js";
|
|
4
|
+
import { getInstalledApplicationPackage, getInstalledVersion } from "./metadata.js";
|
|
5
|
+
import { getAppDirectory, getAppsDirectory } from "./paths.js";
|
|
6
|
+
export async function listAppsCommand() {
|
|
7
|
+
const names = await getApplicationNames();
|
|
8
|
+
if (names.length === 0) {
|
|
9
|
+
console.log("No applications installed");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const rows = await Promise.all(names.map(getApplicationRow));
|
|
13
|
+
printTable(rows);
|
|
14
|
+
}
|
|
15
|
+
async function getApplicationNames() {
|
|
16
|
+
let entries;
|
|
17
|
+
try {
|
|
18
|
+
entries = await fs.readdir(getAppsDirectory(), { withFileTypes: true });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code === "ENOENT") {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return entries
|
|
27
|
+
.filter((entry) => entry.isDirectory())
|
|
28
|
+
.map((entry) => entry.name)
|
|
29
|
+
.sort((a, b) => a.localeCompare(b));
|
|
30
|
+
}
|
|
31
|
+
async function getApplicationRow(name) {
|
|
32
|
+
const appDirectory = getAppDirectory(name);
|
|
33
|
+
let packageName = "unknown";
|
|
34
|
+
let version = "unknown";
|
|
35
|
+
try {
|
|
36
|
+
packageName = await getInstalledApplicationPackage(appDirectory, name);
|
|
37
|
+
version = await getInstalledVersion(appDirectory, packageName);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Keep malformed or incomplete application directories visible in the list.
|
|
41
|
+
}
|
|
42
|
+
const [hasConfig, hasInput] = await Promise.all([
|
|
43
|
+
pathExists(path.join(appDirectory, "config.ts")),
|
|
44
|
+
pathExists(path.join(appDirectory, "input.ts")),
|
|
45
|
+
]);
|
|
46
|
+
const config = hasConfig && hasInput
|
|
47
|
+
? "conflict"
|
|
48
|
+
: hasConfig
|
|
49
|
+
? "config.ts"
|
|
50
|
+
: hasInput
|
|
51
|
+
? "input.ts (legacy)"
|
|
52
|
+
: "missing";
|
|
53
|
+
return { name, packageName, version, config };
|
|
54
|
+
}
|
|
55
|
+
function printTable(rows) {
|
|
56
|
+
const values = [
|
|
57
|
+
["NAME", "PACKAGE", "VERSION", "CONFIG"],
|
|
58
|
+
...rows.map((row) => [row.name, row.packageName, row.version, row.config]),
|
|
59
|
+
];
|
|
60
|
+
const widths = values[0].map((_, column) => Math.max(...values.map((row) => row[column].length)));
|
|
61
|
+
console.log(values
|
|
62
|
+
.map((row) => row
|
|
63
|
+
.map((value, column) => value.padEnd(widths[column]))
|
|
64
|
+
.join(" ")
|
|
65
|
+
.trimEnd())
|
|
66
|
+
.join("\n"));
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { valid } from "semver";
|
|
4
|
+
export async function getInstalledApplicationPackage(appDirectory, name) {
|
|
5
|
+
let packageJson;
|
|
6
|
+
try {
|
|
7
|
+
packageJson = JSON.parse(await fs.readFile(path.join(appDirectory, "package.json"), "utf8"));
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error.code === "ENOENT") {
|
|
11
|
+
throw new Error(`Application "${name}" is not installed`);
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Could not read package metadata for application "${name}"`, { cause: error });
|
|
14
|
+
}
|
|
15
|
+
const dependencyNames = Object.keys(packageJson.dependencies ?? {});
|
|
16
|
+
if (dependencyNames.length !== 1) {
|
|
17
|
+
throw new Error(`Application "${name}" must have exactly one application package dependency`);
|
|
18
|
+
}
|
|
19
|
+
return dependencyNames[0];
|
|
20
|
+
}
|
|
21
|
+
export async function getInstalledVersion(appDirectory, packageName) {
|
|
22
|
+
const packageJson = JSON.parse(await fs.readFile(path.join(appDirectory, "node_modules", packageName, "package.json"), "utf8"));
|
|
23
|
+
if (packageJson.version == null || valid(packageJson.version) == null) {
|
|
24
|
+
throw new Error(`Installed ${packageName} does not have a valid semantic version`);
|
|
25
|
+
}
|
|
26
|
+
return packageJson.version;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getSawsHome } from "@saws/core";
|
|
3
|
+
export function getAppsDirectory() {
|
|
4
|
+
return path.join(getSawsHome(), "apps");
|
|
5
|
+
}
|
|
6
|
+
export function getAppDirectory(name) {
|
|
7
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
8
|
+
throw new Error("Application name must start with a letter or number and contain only letters, numbers, dots, underscores, or hyphens");
|
|
9
|
+
}
|
|
10
|
+
return path.join(getAppsDirectory(), name);
|
|
11
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function runProcess(command: string, args: string[], cwd: string): Promise<void>;
|
|
2
|
+
export declare function getProcessOutput(command: string, args: string[], cwd: string): Promise<string>;
|
|
3
|
+
export declare function runSaws(args: string[], cwd: string): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export async function runProcess(command, args, cwd) {
|
|
3
|
+
await new Promise((resolve, reject) => {
|
|
4
|
+
const child = spawn(command, args, {
|
|
5
|
+
cwd,
|
|
6
|
+
stdio: "inherit",
|
|
7
|
+
});
|
|
8
|
+
child.once("error", reject);
|
|
9
|
+
child.once("exit", (code, signal) => {
|
|
10
|
+
if (signal != null) {
|
|
11
|
+
reject(new Error(`${command} exited from signal ${signal}`));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (code !== 0) {
|
|
15
|
+
reject(new Error(`${command} exited with code ${code ?? "unknown"}`));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
resolve();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function getProcessOutput(command, args, cwd) {
|
|
23
|
+
return await new Promise((resolve, reject) => {
|
|
24
|
+
const child = spawn(command, args, {
|
|
25
|
+
cwd,
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
});
|
|
28
|
+
const stdout = [];
|
|
29
|
+
const stderr = [];
|
|
30
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
31
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
32
|
+
child.once("error", reject);
|
|
33
|
+
child.once("exit", (code, signal) => {
|
|
34
|
+
if (signal != null) {
|
|
35
|
+
reject(new Error(`${command} exited from signal ${signal}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (code !== 0) {
|
|
39
|
+
const details = Buffer.concat(stderr).toString("utf8").trim();
|
|
40
|
+
reject(new Error(`${command} exited with code ${code ?? "unknown"}${details.length > 0 ? `: ${details}` : ""}`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
resolve(Buffer.concat(stdout).toString("utf8"));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export async function runSaws(args, cwd) {
|
|
48
|
+
const cliPath = process.argv[1];
|
|
49
|
+
if (cliPath == null) {
|
|
50
|
+
throw new Error("Could not determine the SAWS CLI entry point");
|
|
51
|
+
}
|
|
52
|
+
await runProcess(process.execPath, [cliPath, ...args], cwd);
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function updateAppCommand(name: string): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { gt, prerelease, valid } from "semver";
|
|
4
|
+
import { applicationWrapper, pathExists } from "./files.js";
|
|
5
|
+
import { getInstalledApplicationPackage, getInstalledVersion } from "./metadata.js";
|
|
6
|
+
import { getAppDirectory } from "./paths.js";
|
|
7
|
+
import { getProcessOutput, runProcess } from "./process.js";
|
|
8
|
+
export async function updateAppCommand(name) {
|
|
9
|
+
const appDirectory = getAppDirectory(name);
|
|
10
|
+
const packageName = await getInstalledApplicationPackage(appDirectory, name);
|
|
11
|
+
const installedVersion = await getInstalledVersion(appDirectory, packageName);
|
|
12
|
+
const migrated = await migrateLegacyConfig(appDirectory, packageName);
|
|
13
|
+
const npmTag = prerelease(installedVersion)?.[0] === "beta" ? "beta" : "latest";
|
|
14
|
+
const latestVersion = await getLatestVersion(appDirectory, packageName, npmTag);
|
|
15
|
+
if (!gt(latestVersion, installedVersion)) {
|
|
16
|
+
const status = gt(installedVersion, latestVersion)
|
|
17
|
+
? `is newer than npm ${npmTag}`
|
|
18
|
+
: "is up to date";
|
|
19
|
+
console.log(`Application "${name}" ${status} (${installedVersion})${migrated ? "; migrated input.ts to config.ts" : ""}`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
await runProcess("npm", ["install", "--save-exact", "--no-fund", "--no-audit", `${packageName}@${latestVersion}`], appDirectory);
|
|
23
|
+
await fs.writeFile(path.join(appDirectory, "saws.ts"), applicationWrapper(packageName));
|
|
24
|
+
console.log(`Updated application "${name}" from ${installedVersion} to ${latestVersion}${migrated ? " and migrated input.ts to config.ts" : ""}`);
|
|
25
|
+
}
|
|
26
|
+
async function getLatestVersion(appDirectory, packageName, npmTag) {
|
|
27
|
+
const output = await getProcessOutput("npm", ["view", `${packageName}@${npmTag}`, "version", "--json"], appDirectory);
|
|
28
|
+
const version = JSON.parse(output);
|
|
29
|
+
if (typeof version !== "string" || valid(version) == null) {
|
|
30
|
+
throw new Error(`npm ${npmTag} for ${packageName} is not a valid semantic version`);
|
|
31
|
+
}
|
|
32
|
+
return version;
|
|
33
|
+
}
|
|
34
|
+
async function migrateLegacyConfig(appDirectory, packageName) {
|
|
35
|
+
const inputPath = path.join(appDirectory, "input.ts");
|
|
36
|
+
const configPath = path.join(appDirectory, "config.ts");
|
|
37
|
+
const [hasInput, hasConfig] = await Promise.all([pathExists(inputPath), pathExists(configPath)]);
|
|
38
|
+
if (hasInput && hasConfig) {
|
|
39
|
+
throw new Error(`Application contains both input.ts and config.ts; remove one before updating so SAWS does not overwrite configuration`);
|
|
40
|
+
}
|
|
41
|
+
if (!hasInput && !hasConfig) {
|
|
42
|
+
throw new Error(`Application configuration is missing; expected config.ts or legacy input.ts`);
|
|
43
|
+
}
|
|
44
|
+
if (!hasInput) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const wrapperPath = path.join(appDirectory, "saws.ts");
|
|
48
|
+
const pendingWrapperPath = path.join(appDirectory, ".saws.ts.update");
|
|
49
|
+
await fs.writeFile(pendingWrapperPath, applicationWrapper(packageName), { flag: "wx" });
|
|
50
|
+
try {
|
|
51
|
+
await fs.rename(inputPath, configPath);
|
|
52
|
+
await fs.rename(pendingWrapperPath, wrapperPath);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (await pathExists(configPath)) {
|
|
56
|
+
await fs.rename(configPath, inputPath).catch(() => undefined);
|
|
57
|
+
}
|
|
58
|
+
await fs.rm(pendingWrapperPath, { force: true });
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const deployCommand: (path: string, { stage }: {
|
|
2
2
|
stage: string;
|
|
3
|
-
|
|
4
|
-
rootDir?: string;
|
|
5
|
-
config?: string;
|
|
6
|
-
}
|
|
7
|
-
export declare function deployCommand(path: string | undefined, options: DeployCommandOptions): Promise<void>;
|
|
8
|
-
//# sourceMappingURL=command.d.ts.map
|
|
3
|
+
}) => Promise<void>;
|
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
export async
|
|
4
|
-
if (
|
|
5
|
-
throw new Error("deploy requires --stage <
|
|
1
|
+
// import { createCacheDir } from "@saws/utils/create-directories";
|
|
2
|
+
import { getSawsConfig } from "@saws/core";
|
|
3
|
+
export const deployCommand = async (path, { stage }) => {
|
|
4
|
+
if (stage == null || stage.length === 0) {
|
|
5
|
+
throw new Error("deploy requires --stage <string>");
|
|
6
6
|
}
|
|
7
|
-
if (
|
|
8
|
-
|
|
7
|
+
if (stage === "local") {
|
|
8
|
+
console.warn("Can not deploy to local stage");
|
|
9
|
+
process.exit();
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
await serviceDefinition.deploy(new DeployContext({
|
|
16
|
-
stage: options.stage,
|
|
17
|
-
rootDir: options.rootDir ?? process.cwd(),
|
|
18
|
-
dryRun,
|
|
19
|
-
env: process.env,
|
|
20
|
-
}));
|
|
21
|
-
}
|
|
11
|
+
process.env.STAGE = stage;
|
|
12
|
+
// await createCacheDir();
|
|
13
|
+
const serviceDefinition = await getSawsConfig(path);
|
|
14
|
+
await serviceDefinition.deploy(stage);
|
|
15
|
+
};
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { deployCommand } from "./command.js";
|
|
3
3
|
export const createCommand = () => new Command("deploy")
|
|
4
|
+
.option("--stage <string>", "stage to deploy")
|
|
4
5
|
.argument("[string]", "path to service definition")
|
|
5
|
-
.requiredOption("--stage <string>", "stage to deploy")
|
|
6
|
-
.option("--dry-run", "print remote commands instead of executing them")
|
|
7
|
-
.option("--root-dir <string>", "project root used for generated local files")
|
|
8
|
-
.option("--config <string>", "path to service definition")
|
|
9
6
|
.action(deployCommand);
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
rootDir?: string;
|
|
3
|
-
config?: string;
|
|
4
|
-
dryRun?: boolean;
|
|
5
|
-
}
|
|
6
|
-
export declare function devCommand(path: string | undefined, options: DevCommandOptions): Promise<void>;
|
|
7
|
-
//# sourceMappingURL=command.d.ts.map
|
|
1
|
+
export declare const devCommand: (path: string) => Promise<void>;
|
|
@@ -1,63 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createCacheDir } from "@saws/core/utils/create-directories";
|
|
2
|
+
import { onProcessExit } from "@saws/core/utils/on-exit";
|
|
3
|
+
import { getSawsConfig } from "@saws/core";
|
|
2
4
|
import { DevTui } from "./tui/dev-tui.js";
|
|
3
|
-
const
|
|
4
|
-
export async function devCommand(path, options) {
|
|
5
|
+
export const devCommand = async (path) => {
|
|
5
6
|
process.env.NODE_ENV = "development";
|
|
6
|
-
process.env.STAGE =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
7
|
+
process.env.STAGE = "local";
|
|
8
|
+
process.env.AWS_REGION = "us-west-2";
|
|
9
|
+
await createCacheDir();
|
|
10
|
+
const serviceDefinition = await getSawsConfig(path);
|
|
11
|
+
const services = collectServices(serviceDefinition);
|
|
10
12
|
const useTui = process.stdout.isTTY && process.stdin.isTTY;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const shutdown = async () => {
|
|
13
|
+
const tui = new DevTui(services.flatMap((service) => [service.name, ...service.getOnDevLogTabs()]));
|
|
14
|
+
const shutdown = () => {
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
+
serviceDefinition.exit();
|
|
16
17
|
}
|
|
17
18
|
finally {
|
|
18
19
|
tui.stop();
|
|
19
|
-
process.exit();
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
process.once("SIGTERM",
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
logSink: useTui ? tui.logSink : undefined,
|
|
31
|
-
}));
|
|
32
|
-
// Service dev hooks start their processes without blocking so every
|
|
33
|
-
// dependency can come up. Keep the command alive until a signal invokes
|
|
34
|
-
// the shared shutdown path.
|
|
35
|
-
if (!options.dryRun) {
|
|
36
|
-
await new Promise(() => { });
|
|
37
|
-
}
|
|
22
|
+
onProcessExit(shutdown);
|
|
23
|
+
process.once("SIGTERM", () => {
|
|
24
|
+
shutdown();
|
|
25
|
+
process.exit();
|
|
26
|
+
});
|
|
27
|
+
if (useTui) {
|
|
28
|
+
serviceDefinition.setRuntimeLogSink(tui.logSink);
|
|
29
|
+
tui.start();
|
|
38
30
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
await serviceDefinition.dev();
|
|
32
|
+
if (useTui) {
|
|
33
|
+
for (const service of services) {
|
|
34
|
+
service.getStdOut()?.on("data", (chunk) => {
|
|
35
|
+
tui.logSink({
|
|
36
|
+
serviceName: service.name,
|
|
37
|
+
stream: "stdout",
|
|
38
|
+
chunk: chunk.toString("utf8"),
|
|
39
|
+
timestamp: new Date(),
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
service.getStdErr()?.on("data", (chunk) => {
|
|
43
|
+
tui.logSink({
|
|
44
|
+
serviceName: service.name,
|
|
45
|
+
stream: "stderr",
|
|
46
|
+
chunk: chunk.toString("utf8"),
|
|
47
|
+
timestamp: new Date(),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
42
50
|
}
|
|
43
|
-
throw error;
|
|
44
51
|
}
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
else {
|
|
53
|
+
for (const service of services) {
|
|
54
|
+
service.getStdOut()?.pipe(process.stdout);
|
|
55
|
+
service.getStdErr()?.pipe(process.stderr);
|
|
56
|
+
}
|
|
47
57
|
}
|
|
48
|
-
}
|
|
58
|
+
};
|
|
49
59
|
function collectServices(root) {
|
|
50
|
-
|
|
51
|
-
const seen = new Set();
|
|
52
|
-
const visit = (service) => {
|
|
53
|
-
if (seen.has(service))
|
|
54
|
-
return;
|
|
55
|
-
seen.add(service);
|
|
56
|
-
for (const dependency of service.dependencies) {
|
|
57
|
-
visit(dependency);
|
|
58
|
-
}
|
|
59
|
-
services.push(service.name);
|
|
60
|
-
};
|
|
61
|
-
visit(root);
|
|
62
|
-
return services;
|
|
60
|
+
return [...new Set(root.getAllDependencies())];
|
|
63
61
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { devCommand } from "./command.js";
|
|
3
|
-
export const createCommand = () => new Command("dev")
|
|
4
|
-
.argument("[string]", "path to service definition")
|
|
5
|
-
.option("--dry-run", "print local Docker commands instead of executing them")
|
|
6
|
-
.option("--root-dir <string>", "project root used for generated local files")
|
|
7
|
-
.option("--config <string>", "path to service definition")
|
|
8
|
-
.action(devCommand);
|
|
3
|
+
export const createCommand = () => new Command("dev").argument("[string]", "path to service definition").action(devCommand);
|
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
export interface RuntimeLogEntry {
|
|
2
|
+
serviceName: string;
|
|
3
|
+
stream: "stdout" | "stderr";
|
|
4
|
+
chunk: string;
|
|
5
|
+
timestamp: Date;
|
|
6
|
+
}
|
|
7
|
+
export type RuntimeLogSink = (entry: RuntimeLogEntry) => void;
|
|
2
8
|
export declare class DevTui {
|
|
3
9
|
readonly logSink: RuntimeLogSink;
|
|
4
10
|
private readonly services;
|
|
5
11
|
private readonly logs;
|
|
12
|
+
private readonly logScrollOffsets;
|
|
6
13
|
private selectedIndex;
|
|
7
14
|
private isStarted;
|
|
15
|
+
private isSelectionMode;
|
|
8
16
|
private previousRawMode;
|
|
9
17
|
private readonly maxLines;
|
|
10
18
|
constructor(services: string[]);
|
|
11
19
|
start(): void;
|
|
12
20
|
stop(): void;
|
|
13
|
-
writeSystemLog(message: string, stream?: "stdout" | "stderr"): void;
|
|
14
21
|
private handleKeypress;
|
|
22
|
+
private handleInput;
|
|
15
23
|
private addLog;
|
|
16
24
|
private render;
|
|
25
|
+
private enterSelectionMode;
|
|
26
|
+
private exitSelectionMode;
|
|
27
|
+
private getHelpText;
|
|
28
|
+
private getSelectedService;
|
|
29
|
+
private getBodyHeight;
|
|
30
|
+
private scrollSelectedLog;
|
|
31
|
+
private selectServiceAt;
|
|
32
|
+
private clampLogScrollOffset;
|
|
17
33
|
}
|
|
18
|
-
//# sourceMappingURL=dev-tui.d.ts.map
|