@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.
Files changed (83) hide show
  1. package/README.md +206 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/bin/saws.d.ts +0 -1
  4. package/dist/bin/saws.js +32 -40
  5. package/dist/commands/app/deploy.d.ts +4 -0
  6. package/dist/commands/app/deploy.js +20 -0
  7. package/dist/commands/app/files.d.ts +4 -0
  8. package/dist/commands/app/files.js +40 -0
  9. package/dist/commands/app/index.d.ts +2 -0
  10. package/dist/commands/app/index.js +22 -0
  11. package/dist/commands/app/install.d.ts +4 -0
  12. package/dist/commands/app/install.js +35 -0
  13. package/dist/commands/app/list.d.ts +1 -0
  14. package/dist/commands/app/list.js +67 -0
  15. package/dist/commands/app/metadata.d.ts +2 -0
  16. package/dist/commands/app/metadata.js +27 -0
  17. package/dist/commands/app/paths.d.ts +2 -0
  18. package/dist/commands/app/paths.js +11 -0
  19. package/dist/commands/app/process.d.ts +3 -0
  20. package/dist/commands/app/process.js +53 -0
  21. package/dist/commands/app/update.d.ts +1 -0
  22. package/dist/commands/app/update.js +62 -0
  23. package/dist/commands/deploy/command.d.ts +2 -7
  24. package/dist/commands/deploy/command.js +13 -19
  25. package/dist/commands/deploy/index.d.ts +0 -1
  26. package/dist/commands/deploy/index.js +1 -4
  27. package/dist/commands/dev/command.d.ts +1 -7
  28. package/dist/commands/dev/command.js +46 -48
  29. package/dist/commands/dev/index.d.ts +0 -1
  30. package/dist/commands/dev/index.js +1 -6
  31. package/dist/commands/dev/tui/dev-tui.d.ts +18 -3
  32. package/dist/commands/dev/tui/dev-tui.js +263 -29
  33. package/dist/commands/host/command.d.ts +3 -2
  34. package/dist/commands/host/command.js +112 -7
  35. package/dist/commands/host/create.d.ts +9 -0
  36. package/dist/commands/host/create.js +30 -0
  37. package/dist/commands/host/deployment-key.d.ts +2 -0
  38. package/dist/commands/host/deployment-key.js +40 -0
  39. package/dist/commands/host/import-key.d.ts +5 -0
  40. package/dist/commands/host/import-key.js +40 -0
  41. package/dist/commands/host/index.d.ts +0 -1
  42. package/dist/commands/host/index.js +25 -3
  43. package/dist/commands/init/command.d.ts +1 -7
  44. package/dist/commands/init/command.js +24 -15
  45. package/dist/commands/init/index.d.ts +0 -1
  46. package/dist/commands/init/index.js +1 -5
  47. package/dist/commands/init/templates/gitignore.template.d.ts +1 -0
  48. package/dist/commands/init/templates/gitignore.template.js +4 -0
  49. package/dist/commands/init/templates/saws-ts.template.d.ts +3 -0
  50. package/dist/commands/init/templates/saws-ts.template.js +7 -0
  51. package/dist/commands/init/templates/tsconfig-json.template.d.ts +1 -0
  52. package/dist/commands/init/templates/tsconfig-json.template.js +4 -0
  53. package/dist/commands/logs/command.d.ts +5 -0
  54. package/dist/commands/logs/command.js +19 -0
  55. package/dist/commands/logs/index.d.ts +2 -0
  56. package/dist/commands/logs/index.js +7 -0
  57. package/dist/commands/secrets/command.d.ts +2 -2
  58. package/dist/commands/secrets/command.js +21 -9
  59. package/dist/commands/secrets/index.d.ts +0 -1
  60. package/dist/commands/secrets/index.js +7 -5
  61. package/dist/hosts.d.ts +1 -3
  62. package/dist/hosts.js +11 -10
  63. package/docs/global-hosts-and-applications.md +307 -0
  64. package/package.json +13 -20
  65. package/dist/bin/saws.d.ts.map +0 -1
  66. package/dist/commands/deploy/command.d.ts.map +0 -1
  67. package/dist/commands/deploy/index.d.ts.map +0 -1
  68. package/dist/commands/dev/command.d.ts.map +0 -1
  69. package/dist/commands/dev/index.d.ts.map +0 -1
  70. package/dist/commands/dev/tui/dev-tui.d.ts.map +0 -1
  71. package/dist/commands/host/command.d.ts.map +0 -1
  72. package/dist/commands/host/command.test.d.ts +0 -2
  73. package/dist/commands/host/command.test.d.ts.map +0 -1
  74. package/dist/commands/host/command.test.js +0 -13
  75. package/dist/commands/host/index.d.ts.map +0 -1
  76. package/dist/commands/init/command.d.ts.map +0 -1
  77. package/dist/commands/init/command.test.d.ts +0 -2
  78. package/dist/commands/init/command.test.d.ts.map +0 -1
  79. package/dist/commands/init/command.test.js +0 -40
  80. package/dist/commands/init/index.d.ts.map +0 -1
  81. package/dist/commands/secrets/command.d.ts.map +0 -1
  82. package/dist/commands/secrets/index.d.ts.map +0 -1
  83. 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,2 @@
1
+ import { Command } from "commander";
2
+ export declare const createCommand: () => Command;
@@ -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,4 @@
1
+ export interface InstallAppOptions {
2
+ name: string;
3
+ }
4
+ export declare function installAppCommand(packageSpecifier: string, options: InstallAppOptions): Promise<void>;
@@ -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,2 @@
1
+ export declare function getInstalledApplicationPackage(appDirectory: string, name: string): Promise<string>;
2
+ export declare function getInstalledVersion(appDirectory: string, packageName: string): Promise<string>;
@@ -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,2 @@
1
+ export declare function getAppsDirectory(): string;
2
+ export declare function getAppDirectory(name: string): string;
@@ -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 interface DeployCommandOptions {
1
+ export declare const deployCommand: (path: string, { stage }: {
2
2
  stage: string;
3
- dryRun?: boolean;
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 { DeployContext, getSawsConfig } from "@saws/core";
2
- import { findConfiguredHosts } from "../../hosts.js";
3
- export async function deployCommand(path, options) {
4
- if (options.stage == null || options.stage.length === 0) {
5
- throw new Error("deploy requires --stage <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 (options.stage === "local") {
8
- throw new Error("Can not deploy to local stage");
7
+ if (stage === "local") {
8
+ console.warn("Can not deploy to local stage");
9
+ process.exit();
9
10
  }
10
- const serviceDefinition = await getSawsConfig(options.config ?? path);
11
- const dryRun = options.dryRun ?? false;
12
- for (const host of findConfiguredHosts(serviceDefinition)) {
13
- await host.assertReady({ dryRun });
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,3 +1,2 @@
1
1
  import { Command } from "commander";
2
2
  export declare const createCommand: () => Command;
3
- //# sourceMappingURL=index.d.ts.map
@@ -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 interface DevCommandOptions {
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 { DevContext, ExitContext, getSawsConfig, } from "@saws/core";
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 LOCAL_STAGE = "local";
4
- export async function devCommand(path, options) {
5
+ export const devCommand = async (path) => {
5
6
  process.env.NODE_ENV = "development";
6
- process.env.STAGE = LOCAL_STAGE;
7
- const rootDir = options.rootDir ?? process.cwd();
8
- const serviceDefinition = await getSawsConfig(options.config ?? path);
9
- const tui = new DevTui(collectServices(serviceDefinition));
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
- if (useTui)
12
- tui.start();
13
- const shutdown = async () => {
13
+ const tui = new DevTui(services.flatMap((service) => [service.name, ...service.getOnDevLogTabs()]));
14
+ const shutdown = () => {
14
15
  try {
15
- await serviceDefinition.exit(new ExitContext({ stage: LOCAL_STAGE, rootDir }));
16
+ serviceDefinition.exit();
16
17
  }
17
18
  finally {
18
19
  tui.stop();
19
- process.exit();
20
20
  }
21
21
  };
22
- process.once("SIGINT", shutdown);
23
- process.once("SIGTERM", shutdown);
24
- try {
25
- await serviceDefinition.dev(new DevContext({
26
- stage: LOCAL_STAGE,
27
- rootDir,
28
- dryRun: options.dryRun ?? false,
29
- env: process.env,
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
- catch (error) {
40
- if (useTui) {
41
- tui.writeSystemLog(error.stack ?? String(error), "stderr");
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
- finally {
46
- tui.stop();
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
- const services = [];
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,3 +1,2 @@
1
1
  import { Command } from "commander";
2
2
  export declare const createCommand: () => Command;
3
- //# sourceMappingURL=index.d.ts.map
@@ -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
- import type { RuntimeLogSink } from "@saws/core";
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