@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
@@ -1,10 +1,32 @@
1
1
  import { Command } from "commander";
2
2
  import { configureHostCommand } from "./command.js";
3
+ import { createHostCommand } from "./create.js";
4
+ import { importHostKeyCommand } from "./import-key.js";
3
5
  export const createCommand = () => new Command("host")
4
6
  .description("configure deployment hosts")
7
+ .addCommand(new Command("create")
8
+ .description("create a global deployment host")
9
+ .argument("<name>", "global host name")
10
+ .requiredOption("--address <address>", "host name or IP address")
11
+ .requiredOption("--user <deployment-user>", "persistent deployment account")
12
+ .option("--ssh-port <port>", "SSH port")
13
+ .option("--platform <platform>", 'Docker platform such as "linux/amd64"')
14
+ .option("--exposure <mode>", 'network exposure policy: "tunnel" or "public"')
15
+ .option("--allowed-tcp-port <port...>", "public TCP ports allowed by the host firewall")
16
+ .action(createHostCommand))
17
+ .addCommand(new Command("key")
18
+ .description("manage global host deployment keys")
19
+ .addCommand(new Command("import")
20
+ .description("import an existing private key into encrypted global storage")
21
+ .argument("<name>", "global host name")
22
+ .requiredOption("--file <path>", "path to an SSH private key")
23
+ .option("--force", "replace an existing deployment key")
24
+ .action(importHostKeyCommand)))
5
25
  .addCommand(new Command("configure")
6
- .description("install Docker and apply the configured security policy")
26
+ .description("bootstrap a deployment user and apply host security policy")
7
27
  .argument("[name]", "host name; optional when exactly one host exists")
8
- .option("--config <string>", "path to service definition")
9
- .option("--dry-run", "print the remote configuration command")
28
+ .requiredOption("--user <bootstrap-user>", "existing SSH account used for initial configuration")
29
+ .option("--config <path>", "path to saws.ts")
30
+ .option("--global", "configure a host from the global host registry")
31
+ .option("--dry-run", "describe configuration without making changes")
10
32
  .action(configureHostCommand));
@@ -1,7 +1 @@
1
- export interface InitCommandOptions {
2
- rootDir?: string;
3
- config?: string;
4
- dryRun?: boolean;
5
- }
6
- export declare function initCommand(serviceName: string, path: string | undefined, options: InitCommandOptions): Promise<void>;
7
- //# sourceMappingURL=command.d.ts.map
1
+ export declare const initCommand: (serviceName?: string, configPath?: string) => Promise<void>;
@@ -1,15 +1,24 @@
1
- import { findServiceDefinition, getSawsConfig, InitContext, } from "@saws/core";
2
- export async function initCommand(serviceName, path, options) {
3
- const rootDir = options.rootDir ?? process.cwd();
4
- const rootService = await getSawsConfig(options.config ?? path);
5
- const service = findServiceDefinition(rootService, serviceName);
6
- await service.init(new InitContext({
7
- stage: "local",
8
- rootDir,
9
- dryRun: options.dryRun ?? false,
10
- env: process.env,
11
- logSink: ({ stream, chunk }) => {
12
- (stream === "stderr" ? process.stderr : process.stdout).write(chunk);
13
- },
14
- }));
15
- }
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { findServiceDefinition, getSawsConfig } from "@saws/core";
4
+ import { installDependencies } from "@saws/core/utils/dependency-management";
5
+ import { createFileIfNotExists } from "@saws/core/utils/create-file-if-not-exists";
6
+ import { sawsTsTemplate } from "./templates/saws-ts.template.js";
7
+ import { tsconfigJsonTemplate } from "./templates/tsconfig-json.template.js";
8
+ import { gitignoreTemplate } from "./templates/gitignore.template.js";
9
+ export const initCommand = async (serviceName, configPath) => {
10
+ if (serviceName != null) {
11
+ const serviceDefinition = await getSawsConfig(configPath);
12
+ await findServiceDefinition(serviceDefinition, serviceName).init();
13
+ return;
14
+ }
15
+ const name = path.parse(path.resolve(".")).name;
16
+ // not used for now
17
+ await installDependencies([]);
18
+ await installDependencies(["@saws/core", "typescript", "@tsconfig/node26"], {
19
+ development: true,
20
+ });
21
+ await fs.writeFile("./tsconfig.json", tsconfigJsonTemplate(), {});
22
+ await createFileIfNotExists("./saws.ts", sawsTsTemplate({ name }));
23
+ await createFileIfNotExists("./.gitignore", gitignoreTemplate());
24
+ };
@@ -1,3 +1,2 @@
1
1
  import { Command } from "commander";
2
2
  export declare const createCommand: () => Command;
3
- //# sourceMappingURL=index.d.ts.map
@@ -1,10 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { initCommand } from "./command.js";
3
3
  export const createCommand = () => new Command("init")
4
- .description("bootstrap one configured service and its dependencies")
5
- .argument("<service>", "name of the service to initialize")
4
+ .argument("[service]", "name of the service to initialize")
6
5
  .argument("[config]", "path to service definition")
7
- .option("--root-dir <string>", "project root used for generated files")
8
- .option("--config <string>", "path to service definition")
9
- .option("--dry-run", "allow service initializers to preview their changes")
10
6
  .action(initCommand);
@@ -0,0 +1 @@
1
+ export declare const gitignoreTemplate: () => string;
@@ -0,0 +1,4 @@
1
+ export const gitignoreTemplate = () => `node_modules
2
+ .saws/saws-*-local-output.json
3
+ .saws/.secrets
4
+ .DS_Store`;
@@ -0,0 +1,3 @@
1
+ export declare const sawsTsTemplate: ({ name }: {
2
+ name: string;
3
+ }) => string;
@@ -0,0 +1,7 @@
1
+ export const sawsTsTemplate = ({ name }) => `import { ServiceDefinition } from "@saws/core";
2
+
3
+ export default new ServiceDefinition({
4
+ name: "${name}",
5
+ dependencies: [],
6
+ });
7
+ `;
@@ -0,0 +1 @@
1
+ export declare const tsconfigJsonTemplate: () => string;
@@ -0,0 +1,4 @@
1
+ export const tsconfigJsonTemplate = () => /* json */ `{
2
+ "extends": "@tsconfig/node26/tsconfig.json",
3
+ "files": []
4
+ }`;
@@ -0,0 +1,5 @@
1
+ export interface LogsCommandOptions {
2
+ config?: string;
3
+ stage: string;
4
+ }
5
+ export declare const logsCommand: (serviceName: string | undefined, options: LogsCommandOptions) => Promise<void>;
@@ -0,0 +1,19 @@
1
+ import { findServiceDefinition, getSawsConfig } from "@saws/core";
2
+ export const logsCommand = async (serviceName, options) => {
3
+ const { config, stage } = options;
4
+ if (stage == null || stage.length === 0) {
5
+ throw new Error("logs requires --stage <string>");
6
+ }
7
+ if (stage === "local")
8
+ return;
9
+ process.env.STAGE = stage;
10
+ const serviceDefinition = await getSawsConfig(config);
11
+ if (serviceName != null) {
12
+ await findServiceDefinition(serviceDefinition, serviceName).logs(stage);
13
+ return;
14
+ }
15
+ await Promise.all(collectServices(serviceDefinition).map((service) => service.logs(stage)));
16
+ };
17
+ function collectServices(root) {
18
+ return [...new Set(root.getAllDependencies())];
19
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const createCommand: () => Command;
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ import { logsCommand } from "./command.js";
3
+ export const createCommand = () => new Command("logs")
4
+ .option("--stage <string>", "stage to tail logs from")
5
+ .option("--config <path>", "path to service definition")
6
+ .argument("[service]", "name of the service to tail logs from")
7
+ .action(logsCommand);
@@ -1,8 +1,8 @@
1
1
  export interface SecretsCommandOptions {
2
+ config?: string;
2
3
  stage?: string;
4
+ global?: boolean;
3
5
  set?: string;
4
6
  get?: boolean;
5
- rootDir?: string;
6
7
  }
7
8
  export declare function secretsCommand(name: string, options: SecretsCommandOptions): Promise<void>;
8
- //# sourceMappingURL=command.d.ts.map
@@ -1,16 +1,28 @@
1
- import { SecretsManager } from "@saws/secrets";
1
+ import { getSawsConfigModule, SecretsManager } from "@saws/core";
2
2
  export async function secretsCommand(name, options) {
3
- const manager = new SecretsManager({
4
- stage: options.stage ?? "local",
5
- rootDir: options.rootDir ?? process.cwd(),
6
- });
3
+ const value = options.set;
4
+ if (options.get && value != null) {
5
+ throw new Error("secrets accepts only one of --get or --set <value>");
6
+ }
7
+ if (!options.get && value == null) {
8
+ throw new Error("secrets requires either --get or --set <value>");
9
+ }
10
+ if (!options.global) {
11
+ process.env.STAGE = options.stage ?? "local";
12
+ }
13
+ const config = await getSawsConfigModule(options.config);
14
+ const manager = config.secrets;
15
+ if (!(manager instanceof SecretsManager)) {
16
+ throw new Error('saws.ts must export a SecretsManager instance named "secrets"');
17
+ }
18
+ const secrets = options.global ? manager.global : manager;
7
19
  if (options.get) {
8
- console.log(await manager.get(name));
20
+ console.log(await secrets.get(name));
9
21
  return;
10
22
  }
11
- if (options.set == null) {
12
- throw new Error("secrets requires either --get or --set <value>");
23
+ if (value == null) {
24
+ throw new Error("secrets requires --set <value>");
13
25
  }
14
- await manager.set(name, options.set);
26
+ await secrets.set(name, value);
15
27
  console.log("Set secret");
16
28
  }
@@ -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,11 @@
1
1
  import { Command } from "commander";
2
2
  import { secretsCommand } from "./command.js";
3
3
  export const createCommand = () => new Command("secrets")
4
- .argument("<string>", "secret name")
5
- .option("--stage <string>", "stage", "local")
6
- .option("--set <string>", "set secret value")
7
- .option("--get", "get secret value")
8
- .option("--root-dir <string>", "project root used for local secrets")
4
+ .description("get or set encrypted project secrets")
5
+ .argument("<name>", "secret name")
6
+ .option("--stage <string>", "stage for a stage-scoped secret", "local")
7
+ .option("--global", "use the global secret scope")
8
+ .option("--set <string>", "set the secret value")
9
+ .option("--get", "get the secret value")
10
+ .option("--config <string>", "path to service definition")
9
11
  .action(secretsCommand);
package/dist/hosts.d.ts CHANGED
@@ -1,4 +1,2 @@
1
- import { Host } from "@saws/host";
2
- import type { ServiceDefinition } from "@saws/core";
1
+ import { Host, type ServiceDefinition } from "@saws/core";
3
2
  export declare function findConfiguredHosts(root: ServiceDefinition): Host[];
4
- //# sourceMappingURL=hosts.d.ts.map
package/dist/hosts.js CHANGED
@@ -1,16 +1,17 @@
1
- import { Host } from "@saws/host";
1
+ import { Host } from "@saws/core";
2
2
  export function findConfiguredHosts(root) {
3
3
  const hosts = new Set();
4
- const visited = new Set();
5
- const visit = (service) => {
6
- if (visited.has(service))
4
+ const visited = new WeakSet();
5
+ const visit = (value) => {
6
+ if (value == null || typeof value !== "object" || visited.has(value))
7
7
  return;
8
- visited.add(service);
9
- const host = service.docker?.host;
10
- if (host instanceof Host)
11
- hosts.add(host);
12
- for (const dependency of service.dependencies)
13
- visit(dependency);
8
+ visited.add(value);
9
+ if (value instanceof Host) {
10
+ hosts.add(value);
11
+ return;
12
+ }
13
+ for (const child of Object.values(value))
14
+ visit(child);
14
15
  };
15
16
  visit(root);
16
17
  return [...hosts];
@@ -0,0 +1,307 @@
1
+ # Global hosts and packaged applications
2
+
3
+ This guide covers the complete remote-deployment workflow:
4
+
5
+ 1. Save a machine as a reusable global host.
6
+ 2. Bootstrap that machine for SAWS deployments.
7
+ 3. Reference the host from an application input file.
8
+ 4. Install and deploy a packaged application.
9
+
10
+ ## Prerequisites
11
+
12
+ You need the SAWS CLI, a Debian or Ubuntu server, and an existing SSH account that can run commands
13
+ as `root` or with interactive `sudo`.
14
+
15
+ ```sh
16
+ npm install --save-dev @saws/cli
17
+ ```
18
+
19
+ By default, global hosts and installed applications live under `~/.saws`. Set `SAWS_HOME` before
20
+ running any host or application command if that state should live elsewhere:
21
+
22
+ ```sh
23
+ export SAWS_HOME=/srv/saws-state
24
+ ```
25
+
26
+ Use the same `SAWS_HOME` for every command that should share hosts, keys, applications, and local
27
+ deployment state.
28
+
29
+ > `saws host configure` applies an opinionated security baseline. It installs and enables Docker,
30
+ > fail2ban, UFW, and unattended upgrades; resets the UFW rules; installs Docker firewall rules;
31
+ > configures a deployment account; and disables SSH password authentication after verifying key
32
+ > access. Use a new server when possible and run the command with `--dry-run` first.
33
+
34
+ ## 1. Create a global host
35
+
36
+ A global host is a named machine profile that can be reused by any project or packaged application.
37
+ Create a public host with:
38
+
39
+ ```sh
40
+ npx saws host create edge \
41
+ --address 203.0.113.10 \
42
+ --user saws \
43
+ --platform linux/amd64 \
44
+ --exposure public
45
+ ```
46
+
47
+ The important values are:
48
+
49
+ - `edge`: the name passed to `getGlobalHost("edge")` later.
50
+ - `--address`: a DNS name or IP address reachable over SSH.
51
+ - `--user`: the persistent, unprivileged account SAWS will create and use for deployments. This is
52
+ not necessarily the account that already exists on the server.
53
+ - `--platform`: the Docker target platform. It is optional, but useful when the development machine
54
+ and server use different CPU architectures. Examples include `linux/amd64` and `linux/arm64`.
55
+ - `--exposure`: `public` or `tunnel`. Public hosts allow selected inbound application ports;
56
+ tunnel hosts allow none.
57
+
58
+ A public host allows TCP ports 80 and 443 by default. Specify the complete public-port set when an
59
+ application needs different ports:
60
+
61
+ ```sh
62
+ npx saws host create observability \
63
+ --address metrics.example.com \
64
+ --user saws \
65
+ --exposure public \
66
+ --allowed-tcp-port 8080 4317 4318
67
+ ```
68
+
69
+ When `--allowed-tcp-port` is supplied, it replaces the default list; include 80 and 443 explicitly
70
+ if the machine also needs them. The SSH port is allowed separately. To use a nonstandard SSH port,
71
+ add `--ssh-port 2222`.
72
+
73
+ For a private machine reached only through an SSH or network tunnel:
74
+
75
+ ```sh
76
+ npx saws host create internal \
77
+ --address 10.0.0.20 \
78
+ --user saws \
79
+ --exposure tunnel
80
+ ```
81
+
82
+ Do not supply public ports for a tunnel host.
83
+
84
+ ### Where the profile is stored
85
+
86
+ The profile is plain JSON at:
87
+
88
+ ```text
89
+ ~/.saws/hosts/<name>/host.json
90
+ ```
91
+
92
+ It contains the address, users, ports, exposure policy, and platform, but no private key. If you edit
93
+ the profile directly, rerun the configure step so the host firewall and readiness record match it.
94
+ Creating a host whose name already exists fails instead of overwriting the profile.
95
+
96
+ ## 2. Configure the machine
97
+
98
+ Preview the bootstrap operation first:
99
+
100
+ ```sh
101
+ npx saws host configure edge --global --user ubuntu --dry-run
102
+ ```
103
+
104
+ Then apply it:
105
+
106
+ ```sh
107
+ npx saws host configure edge --global --user ubuntu
108
+ ```
109
+
110
+ Here, `ubuntu` is an existing bootstrap account with root or sudo access. It is used only to prepare
111
+ the server. Subsequent deployments connect as the persistent deployment user (`saws` in this
112
+ example) configured by the earlier host-creation command.
113
+
114
+ SAWS generates an Ed25519 deployment key when the global host does not have one, encrypts the
115
+ private key in the global secret store, installs the public key for the deployment account, and only
116
+ then disables password-based SSH access.
117
+
118
+ To use an existing unencrypted, non-passphrase-protected private key, import it before configuring
119
+ the machine:
120
+
121
+ ```sh
122
+ npx saws host key import edge --file ~/.ssh/edge_deploy
123
+ npx saws host configure edge --global --user ubuntu
124
+ ```
125
+
126
+ The source file is not modified. Import refuses to replace an existing key unless `--force` is
127
+ passed.
128
+
129
+ Global host keys are encrypted beneath `~/.saws/secrets`. The encryption passcode and generated
130
+ public-key metadata are stored in `~/.saws/.env`, with file permissions restricted to the current
131
+ user. Back up both locations securely. Losing the passcode makes the encrypted deployment key
132
+ unusable.
133
+
134
+ ## 3. Install an application instance
135
+
136
+ Install the package and give this local instance a name:
137
+
138
+ ```sh
139
+ npx saws app install @saws/reverse-proxy-application --name edge-proxy
140
+ ```
141
+
142
+ The package argument accepts an npm version or tag. Pin a version for the initial installation if
143
+ needed:
144
+
145
+ ```sh
146
+ npx saws app install @saws/reverse-proxy-application@2.0.0-beta.17 --name edge-proxy
147
+ ```
148
+
149
+ SAWS refuses to install over an existing application name. It creates this local application
150
+ directory:
151
+
152
+ ```text
153
+ ~/.saws/apps/edge-proxy/
154
+ ├── config.ts
155
+ ├── package.json
156
+ ├── package-lock.json
157
+ ├── saws.ts
158
+ └── node_modules/
159
+ ```
160
+
161
+ This directory also becomes the home for that instance's secrets, outputs, and other local SAWS
162
+ state. The install command does not deploy anything.
163
+
164
+ List installed application instances, package versions, and configuration state at any time:
165
+
166
+ ```sh
167
+ npx saws app list
168
+ ```
169
+
170
+ ## 4. Configure the generated file
171
+
172
+ A packaged application exports the shape of the input it expects. Consult that package's README for
173
+ its application-specific fields. Applications that deploy to a remote machine accept or derive a
174
+ `Host`; reference the global profile with `getGlobalHost`.
175
+
176
+ Open the generated configuration in your editor:
177
+
178
+ ```sh
179
+ ${EDITOR:-vi} "${SAWS_HOME:-$HOME/.saws}/apps/edge-proxy/config.ts"
180
+ ```
181
+
182
+ The generated file derives its type from the installed application's `create` factory. TypeScript
183
+ therefore reports missing or incompatible fields. For example, `config.ts` can route public
184
+ hostnames to ports on the target machine:
185
+
186
+ ```ts
187
+ import { getGlobalHost } from "@saws/core";
188
+ import type { ReverseProxyApplicationConfig } from "@saws/reverse-proxy-application";
189
+
190
+ export default {
191
+ host: getGlobalHost("edge"),
192
+ acmeEmail: "ops@example.com",
193
+ routes: [
194
+ { hostname: "api.example.com", port: 3000 },
195
+ { hostname: "app.example.com", port: 8080, healthUri: "/health" },
196
+ ],
197
+ } satisfies ReverseProxyApplicationConfig;
198
+ ```
199
+
200
+ For applications with secrets, export a `SecretsManager` named `secrets` and pass lazy references
201
+ instead of secret values:
202
+
203
+ ```ts
204
+ import { getGlobalHost, SecretsManager } from "@saws/core";
205
+ import type { OpenDesignApplicationConfig } from "@saws/open-design-application";
206
+
207
+ export const secrets = new SecretsManager();
208
+
209
+ export default {
210
+ host: getGlobalHost("edge"),
211
+ registry: "registry.example.com/saws",
212
+ setupPassword: secrets.reference("open-design-application-password"),
213
+ } satisfies OpenDesignApplicationConfig;
214
+ ```
215
+
216
+ `secrets.reference(...)` is stage-scoped: production and staging can use different values without
217
+ changing the configuration file. Use `secrets.global.reference(...)` only for a value intentionally
218
+ shared across stages.
219
+
220
+ ### Update an installed application
221
+
222
+ Update an instance when the version on its npm release channel is newer than its installed semantic
223
+ version:
224
+
225
+ ```sh
226
+ npx saws app update edge-proxy
227
+ ```
228
+
229
+ Update never overwrites `config.ts`. Stable applications follow npm's `latest` dist-tag, while
230
+ applications already on a beta version follow the `beta` dist-tag. If the instance was installed by
231
+ an older SAWS CLI, its first update renames `input.ts` to `config.ts` without changing the contents.
232
+ If both files exist, update stops and asks you to resolve the ambiguity.
233
+
234
+ ### Set application secrets
235
+
236
+ Run secret commands from the installed application directory so a default `SecretsManager` resolves
237
+ the correct storage location:
238
+
239
+ ```sh
240
+ cd "${SAWS_HOME:-$HOME/.saws}/apps/edge-proxy"
241
+ npx --package @saws/cli saws secrets example-secret \
242
+ --stage production \
243
+ --set 'replace-with-the-secret-value'
244
+ ```
245
+
246
+ Avoid placing real secret values in shell history when that matters for your environment. The
247
+ encrypted stage file is stored under the installed application's `.saws/secrets` directory.
248
+
249
+ ## 5. Deploy the application
250
+
251
+ Deploy a named instance to any non-`local` stage:
252
+
253
+ ```sh
254
+ npx saws app deploy edge-proxy --stage production
255
+ ```
256
+
257
+ The deployment command:
258
+
259
+ 1. Loads `~/.saws/apps/edge-proxy/saws.ts`.
260
+ 2. Resolves the generated configuration and application factory.
261
+ 3. Resolves stage-scoped environment and secrets.
262
+ 4. Connects to the global host with its deployment key.
263
+ 5. Verifies that the host still matches the configured security policy.
264
+ 6. Deploys application dependencies in order and starts or replaces changed containers.
265
+
266
+ By default, stages isolate generated names, Docker networks, persistent volumes, runtime files,
267
+ secrets, and outputs. Deploying `staging` and `production` therefore creates separate application
268
+ resources even when they use the same host. Explicitly overridden volume names can opt out of that
269
+ isolation.
270
+
271
+ For a public application, confirm before deployment that:
272
+
273
+ - DNS records point to the host.
274
+ - Every published TCP port is in the global host's `allowedTcpPorts`.
275
+ - Cloud-provider or upstream-network firewalls allow the same ports.
276
+ - Any required registry credentials and application secrets exist for the target stage.
277
+ - Services addressed by a reverse proxy are reachable from the proxy container.
278
+
279
+ ## Operations and troubleshooting
280
+
281
+ To run standard SAWS commands against an installed application, change into its directory:
282
+
283
+ ```sh
284
+ cd "${SAWS_HOME:-$HOME/.saws}/apps/edge-proxy"
285
+ npx --package @saws/cli saws logs --stage production
286
+ npx --package @saws/cli saws logs reverse-proxy --stage production
287
+ ```
288
+
289
+ Common failures:
290
+
291
+ - **Host is not configured or has drifted:** rerun the global `host configure` command with the host
292
+ name and bootstrap user. This reapplies the declared firewall and security policy.
293
+ - **SSH fails after configuration:** verify that the global key store and `~/.saws/.env` came from
294
+ the same SAWS home and that the configured address and SSH port are correct.
295
+ - **A published service is unreachable:** Docker port publishing and the host allowlist are separate.
296
+ The service must publish the port, and the host profile must allow it.
297
+ - **An HTTPS certificate is not issued:** verify public DNS and inbound ports 80 and 443. A hostname
298
+ route in the reverse-proxy application requests and renews its certificate automatically.
299
+ - **A remote Dockerfile application asks for a registry:** remote hosts cannot use an image that was
300
+ built only on the development machine. Configure the application's `registry` and any required
301
+ registry authentication, or supply a prebuilt image.
302
+ - **A secret cannot be decrypted:** use the same `SAWS_HOME` and `SAWS_SECRETS_PASSCODE` that created
303
+ the encrypted file.
304
+
305
+ Treat `~/.saws` (or the configured `SAWS_HOME`) as operational state. Back it up securely, especially
306
+ the global `.env`, encrypted secret stores, application input files, and any output needed by your
307
+ deployment workflow. Remote Docker volumes must be backed up separately on the target machines.
package/package.json CHANGED
@@ -1,34 +1,27 @@
1
1
  {
2
2
  "name": "@saws/cli",
3
- "version": "2.0.0-beta.2",
4
- "description": "Command-line interface for SAWS",
3
+ "version": "2.0.0-beta.20",
4
+ "description": "",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/shichongrui/saws.git",
8
- "directory": "packages/cli"
7
+ "url": "git+https://github.com/shichongrui/saws.git"
9
8
  },
10
- "type": "module",
11
9
  "bin": {
12
- "saws": "dist/bin/saws.js"
10
+ "saws": "./dist/bin/saws.js"
13
11
  },
14
12
  "files": [
15
- "dist"
13
+ "./dist",
14
+ "./docs"
16
15
  ],
17
- "scripts": {
18
- "prebuild": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
19
- "build": "tsc -p tsconfig.json",
20
- "postbuild": "chmod +x dist/bin/saws.js",
21
- "test": "npm run build && node --test dist/commands/**/*.test.js",
22
- "typecheck": "tsc -p tsconfig.json --noEmit"
23
- },
16
+ "type": "module",
24
17
  "dependencies": {
25
- "@saws/core": "2.0.0-beta.2",
26
- "@saws/host": "2.0.0-beta.2",
27
- "@saws/secrets": "2.0.0-beta.2",
28
- "commander": "^14.0.3"
18
+ "@saws/core": "2.0.0-beta.20",
19
+ "commander": "^15.0.0",
20
+ "find-package-json": "^1.2.0",
21
+ "semver": "^7.8.5"
29
22
  },
30
23
  "devDependencies": {
31
- "@types/node": "^20.11.19",
32
- "typescript": "^5.3.3"
24
+ "@types/find-package-json": "^1.2.7",
25
+ "@types/semver": "^7.8.0"
33
26
  }
34
27
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"saws.d.ts","sourceRoot":"","sources":["../../src/bin/saws.ts"],"names":[],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy/command.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,EAAE,oBAAoB,iBA2B9B"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,eAAO,MAAM,aAAa,eAOA,CAAC"}