@cardelli/ambit 0.1.5 → 0.2.1

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 (140) hide show
  1. package/README.md +261 -0
  2. package/esm/cli/commands/create/index.d.ts +2 -0
  3. package/esm/cli/commands/create/index.d.ts.map +1 -0
  4. package/esm/cli/commands/create/index.js +292 -0
  5. package/esm/cli/commands/create/machine.d.ts +33 -0
  6. package/esm/cli/commands/create/machine.d.ts.map +1 -0
  7. package/esm/cli/commands/create/machine.js +162 -0
  8. package/esm/cli/commands/deploy/index.d.ts +2 -0
  9. package/esm/cli/commands/deploy/index.d.ts.map +1 -0
  10. package/esm/cli/commands/deploy/index.js +290 -0
  11. package/esm/cli/commands/deploy/machine.d.ts +52 -0
  12. package/esm/cli/commands/deploy/machine.d.ts.map +1 -0
  13. package/esm/cli/commands/deploy/machine.js +116 -0
  14. package/esm/cli/commands/deploy/modes.d.ts +18 -0
  15. package/esm/cli/commands/deploy/modes.d.ts.map +1 -0
  16. package/esm/cli/commands/deploy/modes.js +152 -0
  17. package/esm/cli/commands/destroy/app.d.ts +2 -0
  18. package/esm/cli/commands/destroy/app.d.ts.map +1 -0
  19. package/esm/cli/commands/destroy/app.js +173 -0
  20. package/esm/cli/commands/destroy/index.d.ts +2 -0
  21. package/esm/cli/commands/destroy/index.d.ts.map +1 -0
  22. package/esm/cli/commands/destroy/index.js +63 -0
  23. package/esm/cli/commands/destroy/network.d.ts +2 -0
  24. package/esm/cli/commands/destroy/network.d.ts.map +1 -0
  25. package/esm/cli/commands/destroy/network.js +210 -0
  26. package/esm/cli/commands/doctor.d.ts.map +1 -0
  27. package/esm/cli/commands/doctor.js +295 -0
  28. package/esm/{src/cli → cli}/commands/list.d.ts.map +1 -1
  29. package/esm/{src/cli → cli}/commands/list.js +39 -54
  30. package/esm/cli/commands/status.d.ts.map +1 -0
  31. package/esm/cli/commands/status.js +331 -0
  32. package/esm/cli/mod.d.ts.map +1 -0
  33. package/esm/{src/cli → cli}/mod.js +4 -4
  34. package/esm/deno.d.ts +4 -18
  35. package/esm/deno.js +5 -19
  36. package/esm/deps/jsr.io/@std/path/1.1.4/constants.d.ts +1 -1
  37. package/esm/lib/args.d.ts +11 -0
  38. package/esm/lib/args.d.ts.map +1 -0
  39. package/esm/lib/args.js +28 -0
  40. package/esm/lib/cli.d.ts +0 -1
  41. package/esm/lib/cli.d.ts.map +1 -1
  42. package/esm/lib/cli.js +0 -1
  43. package/esm/lib/command.d.ts +0 -3
  44. package/esm/lib/command.d.ts.map +1 -1
  45. package/esm/lib/command.js +2 -13
  46. package/esm/lib/machine.d.ts +11 -0
  47. package/esm/lib/machine.d.ts.map +1 -0
  48. package/esm/lib/machine.js +15 -0
  49. package/esm/lib/output.d.ts +2 -1
  50. package/esm/lib/output.d.ts.map +1 -1
  51. package/esm/lib/output.js +21 -3
  52. package/esm/lib/result.d.ts +0 -1
  53. package/esm/lib/result.d.ts.map +1 -1
  54. package/esm/lib/result.js +0 -1
  55. package/esm/main.d.ts +6 -6
  56. package/esm/main.d.ts.map +1 -1
  57. package/esm/main.js +7 -9
  58. package/esm/providers/fly.d.ts +81 -0
  59. package/esm/providers/fly.d.ts.map +1 -0
  60. package/esm/providers/fly.js +372 -0
  61. package/esm/providers/tailscale.d.ts +31 -0
  62. package/esm/providers/tailscale.d.ts.map +1 -0
  63. package/esm/providers/tailscale.js +150 -0
  64. package/esm/{src/schemas → schemas}/fly.d.ts +1 -11
  65. package/esm/schemas/fly.d.ts.map +1 -0
  66. package/esm/{src/schemas → schemas}/fly.js +14 -56
  67. package/esm/{src/schemas → schemas}/tailscale.d.ts +1 -2
  68. package/esm/schemas/tailscale.d.ts.map +1 -0
  69. package/esm/{src/schemas → schemas}/tailscale.js +2 -3
  70. package/esm/src/{docker/router → router}/Dockerfile +0 -11
  71. package/esm/src/{docker/router → router}/start.sh +18 -9
  72. package/esm/util/constants.d.ts +13 -0
  73. package/esm/util/constants.d.ts.map +1 -0
  74. package/esm/util/constants.js +34 -0
  75. package/esm/{src → util}/credentials.d.ts +0 -1
  76. package/esm/util/credentials.d.ts.map +1 -0
  77. package/esm/{src → util}/credentials.js +3 -5
  78. package/esm/{src → util}/discovery.d.ts +16 -3
  79. package/esm/util/discovery.d.ts.map +1 -0
  80. package/esm/{src → util}/discovery.js +24 -15
  81. package/esm/util/fly-transforms.d.ts +27 -0
  82. package/esm/util/fly-transforms.d.ts.map +1 -0
  83. package/esm/util/fly-transforms.js +87 -0
  84. package/esm/{src → util}/guard.d.ts +1 -2
  85. package/esm/util/guard.d.ts.map +1 -0
  86. package/esm/{src → util}/guard.js +27 -27
  87. package/esm/util/naming.d.ts +5 -0
  88. package/esm/util/naming.d.ts.map +1 -0
  89. package/esm/util/naming.js +12 -0
  90. package/esm/{src → util}/resolve.d.ts +2 -3
  91. package/esm/util/resolve.d.ts.map +1 -0
  92. package/esm/{src → util}/resolve.js +1 -2
  93. package/esm/util/session.d.ts +16 -0
  94. package/esm/util/session.d.ts.map +1 -0
  95. package/esm/util/session.js +19 -0
  96. package/esm/util/tailscale-local.d.ts +13 -0
  97. package/esm/util/tailscale-local.d.ts.map +1 -0
  98. package/esm/util/tailscale-local.js +63 -0
  99. package/esm/{src → util}/template.d.ts +0 -1
  100. package/esm/util/template.d.ts.map +1 -0
  101. package/esm/{src → util}/template.js +0 -1
  102. package/package.json +1 -49
  103. package/esm/lib/paths.d.ts +0 -3
  104. package/esm/lib/paths.d.ts.map +0 -1
  105. package/esm/lib/paths.js +0 -5
  106. package/esm/src/cli/commands/create.d.ts +0 -2
  107. package/esm/src/cli/commands/create.d.ts.map +0 -1
  108. package/esm/src/cli/commands/create.js +0 -308
  109. package/esm/src/cli/commands/deploy.d.ts +0 -2
  110. package/esm/src/cli/commands/deploy.d.ts.map +0 -1
  111. package/esm/src/cli/commands/deploy.js +0 -430
  112. package/esm/src/cli/commands/destroy.d.ts +0 -2
  113. package/esm/src/cli/commands/destroy.d.ts.map +0 -1
  114. package/esm/src/cli/commands/destroy.js +0 -340
  115. package/esm/src/cli/commands/doctor.d.ts.map +0 -1
  116. package/esm/src/cli/commands/doctor.js +0 -141
  117. package/esm/src/cli/commands/status.d.ts.map +0 -1
  118. package/esm/src/cli/commands/status.js +0 -152
  119. package/esm/src/cli/mod.d.ts.map +0 -1
  120. package/esm/src/credentials.d.ts.map +0 -1
  121. package/esm/src/discovery.d.ts.map +0 -1
  122. package/esm/src/guard.d.ts.map +0 -1
  123. package/esm/src/providers/fly.d.ts +0 -76
  124. package/esm/src/providers/fly.d.ts.map +0 -1
  125. package/esm/src/providers/fly.js +0 -407
  126. package/esm/src/providers/tailscale.d.ts +0 -31
  127. package/esm/src/providers/tailscale.d.ts.map +0 -1
  128. package/esm/src/providers/tailscale.js +0 -189
  129. package/esm/src/resolve.d.ts.map +0 -1
  130. package/esm/src/schemas/config.d.ts +0 -5
  131. package/esm/src/schemas/config.d.ts.map +0 -1
  132. package/esm/src/schemas/config.js +0 -22
  133. package/esm/src/schemas/fly.d.ts.map +0 -1
  134. package/esm/src/schemas/tailscale.d.ts.map +0 -1
  135. package/esm/src/template.d.ts.map +0 -1
  136. /package/esm/{src/cli → cli}/commands/doctor.d.ts +0 -0
  137. /package/esm/{src/cli → cli}/commands/list.d.ts +0 -0
  138. /package/esm/{src/cli → cli}/commands/status.d.ts +0 -0
  139. /package/esm/{src/cli → cli}/mod.d.ts +0 -0
  140. /package/esm/src/{docker/router → router}/fly.toml +0 -0
@@ -0,0 +1,162 @@
1
+ // =============================================================================
2
+ // Create — Phases, Context, Hydration, Transitions
3
+ // =============================================================================
4
+ import { randomId } from "../../../lib/cli.js";
5
+ import { Result } from "../../../lib/result.js";
6
+ import { extractSubnet } from "../../../util/fly-transforms.js";
7
+ import { ROUTER_DOCKER_DIR, SECRET_NETWORK_NAME, SECRET_ROUTER_ID, SECRET_TAILSCALE_AUTHKEY, } from "../../../util/constants.js";
8
+ import { FlyDeployError, } from "../../../providers/fly.js";
9
+ import { getRouterAppName } from "../../../util/naming.js";
10
+ import { enableAcceptRoutes, isAcceptRoutesEnabled, isAutoApproverConfigured, isTailscaleInstalled, waitForDevice, } from "../../../util/tailscale-local.js";
11
+ import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
12
+ // =============================================================================
13
+ // Phase Labels
14
+ // =============================================================================
15
+ const CREATE_PHASES = [
16
+ { phase: "create_app", label: "Fly App Created" },
17
+ { phase: "deploy_router", label: "Router Deployed" },
18
+ { phase: "await_device", label: "Router in Tailnet" },
19
+ { phase: "approve_routes", label: "Routes Approved" },
20
+ { phase: "configure_dns", label: "Split DNS Configured" },
21
+ { phase: "accept_routes", label: "Accept Routes Enabled" },
22
+ ];
23
+ export const reportSkipped = (out, startPhase) => {
24
+ for (const { phase, label } of CREATE_PHASES) {
25
+ if (phase === startPhase)
26
+ break;
27
+ out.skip(label);
28
+ }
29
+ };
30
+ // =============================================================================
31
+ // Hydration — determine starting phase from infrastructure state
32
+ // =============================================================================
33
+ export const hydrateCreate = async (ctx) => {
34
+ const router = await findRouterApp(ctx.fly, ctx.org, ctx.network);
35
+ if (!router)
36
+ return "create_app";
37
+ ctx.appName = router.appName;
38
+ ctx.routerId = router.routerId;
39
+ const machine = await getRouterMachineInfo(ctx.fly, router.appName);
40
+ if (!machine || machine.state !== "started")
41
+ return "deploy_router";
42
+ ctx.subnet = machine.subnet;
43
+ if (!ctx.shouldApprove)
44
+ return "complete";
45
+ const device = await ctx.tailscale.devices.getByHostname(router.appName);
46
+ if (!device)
47
+ return "await_device";
48
+ ctx.device = device;
49
+ const routes = await ctx.tailscale.routes.get(device.id);
50
+ if (!routes || routes.unapproved.length > 0)
51
+ return "approve_routes";
52
+ const dns = await ctx.tailscale.dns.getSplit();
53
+ if (!dns[ctx.network]?.length)
54
+ return "configure_dns";
55
+ if (!(await isAcceptRoutesEnabled()))
56
+ return "accept_routes";
57
+ return "complete";
58
+ };
59
+ // =============================================================================
60
+ // Transitions
61
+ // =============================================================================
62
+ export const createTransition = async (phase, ctx) => {
63
+ switch (phase) {
64
+ case "create_app": {
65
+ const suffix = randomId(8);
66
+ ctx.appName = getRouterAppName(ctx.network, suffix);
67
+ ctx.routerId = suffix;
68
+ await ctx.out.spin("Creating App", () => ctx.fly.apps.create(ctx.appName, ctx.org, { network: ctx.network }));
69
+ ctx.out.ok(`Created App: ${ctx.appName}`);
70
+ return Result.ok("deploy_router");
71
+ }
72
+ case "deploy_router": {
73
+ const authKey = await ctx.tailscale.auth.createKey({
74
+ reusable: false,
75
+ ephemeral: false,
76
+ preauthorized: true,
77
+ tags: [ctx.tag],
78
+ });
79
+ ctx.out.ok("Auth Key Created");
80
+ await ctx.out.spin("Setting Secrets", () => ctx.fly.secrets.set(ctx.appName, {
81
+ [SECRET_TAILSCALE_AUTHKEY]: authKey,
82
+ [SECRET_NETWORK_NAME]: ctx.network,
83
+ [SECRET_ROUTER_ID]: ctx.routerId,
84
+ }, { stage: true }));
85
+ const dockerDir = ROUTER_DOCKER_DIR;
86
+ try {
87
+ await ctx.fly.deploy.router(ctx.appName, dockerDir, {
88
+ region: ctx.region,
89
+ });
90
+ }
91
+ catch (e) {
92
+ if (e instanceof FlyDeployError) {
93
+ ctx.out.dim(` ${e.detail}`);
94
+ return Result.err(e.message);
95
+ }
96
+ throw e;
97
+ }
98
+ ctx.out.ok("Router Deployed");
99
+ const machines = await ctx.fly.machines.list(ctx.appName);
100
+ const m = machines.find((m) => m.private_ip);
101
+ if (m?.private_ip)
102
+ ctx.subnet = extractSubnet(m.private_ip);
103
+ if (!ctx.shouldApprove)
104
+ return Result.ok("complete");
105
+ return Result.ok("await_device");
106
+ }
107
+ case "await_device": {
108
+ ctx.device = await waitForDevice(ctx.tailscale, ctx.appName, 180000);
109
+ ctx.out.ok(`Router Joined Tailnet: ${ctx.device.addresses[0]}`);
110
+ if (!ctx.subnet) {
111
+ const machines = await ctx.fly.machines.list(ctx.appName);
112
+ const m = machines.find((m) => m.private_ip);
113
+ if (m?.private_ip)
114
+ ctx.subnet = extractSubnet(m.private_ip);
115
+ }
116
+ return Result.ok("approve_routes");
117
+ }
118
+ case "approve_routes": {
119
+ if (!ctx.device || !ctx.subnet) {
120
+ return Result.err("Missing Device or Subnet");
121
+ }
122
+ const policy = await ctx.tailscale.acl.getPolicy();
123
+ const hasAutoApprover = isAutoApproverConfigured(policy, ctx.tag);
124
+ if (hasAutoApprover) {
125
+ ctx.out.ok("Routes Auto-Approved via ACL Policy");
126
+ }
127
+ else {
128
+ await ctx.tailscale.routes.approve(ctx.device.id, [ctx.subnet]);
129
+ ctx.out.ok("Subnet Routes Approved");
130
+ }
131
+ return Result.ok("configure_dns");
132
+ }
133
+ case "configure_dns": {
134
+ if (!ctx.device)
135
+ return Result.err("Missing Device");
136
+ await ctx.tailscale.dns.setSplit(ctx.network, [ctx.device.addresses[0]]);
137
+ ctx.out.ok(`Split DNS Configured: *.${ctx.network}`);
138
+ return Result.ok("accept_routes");
139
+ }
140
+ case "accept_routes": {
141
+ if (await isTailscaleInstalled()) {
142
+ if (await isAcceptRoutesEnabled()) {
143
+ ctx.out.ok("Accept Routes Already Enabled");
144
+ }
145
+ else if (await enableAcceptRoutes()) {
146
+ ctx.out.ok("Accept Routes Enabled");
147
+ }
148
+ else {
149
+ ctx.out.warn("Could Not Enable Accept Routes");
150
+ ctx.out.dim(" Run: sudo tailscale set --accept-routes");
151
+ }
152
+ }
153
+ else {
154
+ ctx.out.warn("Tailscale CLI Not Found");
155
+ ctx.out.dim(" Ensure Accept-Routes is Enabled on This Device");
156
+ }
157
+ return Result.ok("complete");
158
+ }
159
+ default:
160
+ return Result.err(`Unknown Phase: ${phase}`);
161
+ }
162
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,290 @@
1
+ // =============================================================================
2
+ // Deploy Command - Safely Deploy a Workload App on a Custom Private Network
3
+ // =============================================================================
4
+ import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
5
+ import { bold } from "../../../lib/cli.js";
6
+ import { checkArgs } from "../../../lib/args.js";
7
+ import { createOutput } from "../../../lib/output.js";
8
+ import { runMachine } from "../../../lib/machine.js";
9
+ import { registerCommand } from "../../mod.js";
10
+ import { createFlyProvider, } from "../../../providers/fly.js";
11
+ import { getWorkloadAppName } from "../../../util/naming.js";
12
+ import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
13
+ import { resolveOrg } from "../../../util/resolve.js";
14
+ import { assertNotRouter } from "../../../util/guard.js";
15
+ import { resolveConfigMode, resolveImageMode, resolveTemplateMode, } from "./modes.js";
16
+ import { deployTransition, hydrateDeploy, reportDeploySkipped, } from "./machine.js";
17
+ // =============================================================================
18
+ // Stage 1: Fly.io Configuration
19
+ // =============================================================================
20
+ const stageFlyConfig = async (out, opts) => {
21
+ out.header("Step 1: Fly.io Configuration").blank();
22
+ const fly = createFlyProvider();
23
+ await fly.auth.ensureInstalled();
24
+ const email = await fly.auth.login({ interactive: !opts.json });
25
+ out.ok(`Authenticated as ${email}`);
26
+ const org = await resolveOrg(fly, opts, out);
27
+ out.blank();
28
+ return { fly, org };
29
+ };
30
+ // =============================================================================
31
+ // Stage 2: Network Verification
32
+ // =============================================================================
33
+ const stageNetworkVerification = async (out, fly, opts) => {
34
+ out.header("Step 2: Network Verification").blank();
35
+ const routerSpinner = out.spinner("Checking for Router on Network");
36
+ const router = await findRouterApp(fly, opts.org, opts.network);
37
+ if (!router) {
38
+ routerSpinner.fail("No Router Found");
39
+ return out.die(`No Ambit Router Found on Network '${opts.network}'. ` +
40
+ `Run 'ambit create ${opts.network}' First.`);
41
+ }
42
+ routerSpinner.success(`Router Found: ${router.appName}`);
43
+ const routerMachine = await getRouterMachineInfo(fly, router.appName);
44
+ out.blank();
45
+ return {
46
+ routerId: router.routerId,
47
+ flyAppName: getWorkloadAppName(opts.app, router.routerId),
48
+ routerPrivateIp: routerMachine?.privateIp,
49
+ };
50
+ };
51
+ // =============================================================================
52
+ // Stage 3: Pre-flight Check
53
+ // =============================================================================
54
+ const stagePreflightCheck = async (out, opts) => {
55
+ out.header("Step 3: Pre-flight Check").blank();
56
+ let deployConfig;
57
+ if (opts.template) {
58
+ deployConfig = await resolveTemplateMode(opts.template, out);
59
+ }
60
+ else if (opts.image) {
61
+ deployConfig = resolveImageMode(opts.image, opts.mainPort, out);
62
+ }
63
+ else {
64
+ deployConfig = await resolveConfigMode(opts.config, out);
65
+ }
66
+ if (!deployConfig)
67
+ return out.die("Pre-flight Check Failed");
68
+ out.blank();
69
+ return deployConfig;
70
+ };
71
+ // =============================================================================
72
+ // Stage 4: Deploy
73
+ // =============================================================================
74
+ const stageDeploy = async (out, fly, deployConfig, opts) => {
75
+ out.header("Step 4: Deploy").blank();
76
+ const ctx = {
77
+ fly,
78
+ out,
79
+ ...opts,
80
+ created: false,
81
+ deployConfig,
82
+ deployOptions: {
83
+ routerId: opts.routerId,
84
+ image: deployConfig.image,
85
+ config: deployConfig.configPath,
86
+ region: opts.region,
87
+ },
88
+ };
89
+ const phase = await hydrateDeploy(ctx);
90
+ reportDeploySkipped(out, phase);
91
+ const machine = {
92
+ terminal: "complete",
93
+ transition: deployTransition,
94
+ };
95
+ const result = await runMachine(machine, phase, ctx);
96
+ if (!result.ok) {
97
+ if (result.error === "Cancelled")
98
+ return;
99
+ return out.die(result.error);
100
+ }
101
+ stageSummary(out, ctx, deployConfig);
102
+ };
103
+ // =============================================================================
104
+ // Stage 5: Summary
105
+ // =============================================================================
106
+ const stageSummary = (out, ctx, deployConfig) => {
107
+ const audit = ctx.audit ?? {
108
+ public_ips_released: 0,
109
+ certs_removed: 0,
110
+ flycast_allocations: [],
111
+ warnings: [],
112
+ };
113
+ const hasIssues = audit.public_ips_released > 0 || audit.warnings.length > 0;
114
+ const resultData = {
115
+ app: ctx.app,
116
+ network: ctx.network,
117
+ created: ctx.created,
118
+ audit,
119
+ preflight: deployConfig.preflight,
120
+ };
121
+ if (hasIssues) {
122
+ out.fail("Deploy Completed with Issues", resultData);
123
+ }
124
+ else {
125
+ out.done(resultData);
126
+ }
127
+ out.blank()
128
+ .header("=".repeat(50))
129
+ .header(hasIssues
130
+ ? " Deploy Completed (with Warnings)"
131
+ : " Deploy Completed!")
132
+ .header("=".repeat(50))
133
+ .blank()
134
+ .text(`App '${ctx.app}' Is Reachable from Your Tailnet as:`)
135
+ .text(` ${ctx.app}.${ctx.network}`)
136
+ .blank();
137
+ out.print();
138
+ };
139
+ // =============================================================================
140
+ // Deploy Command
141
+ // =============================================================================
142
+ const deploy = async (argv) => {
143
+ const opts = {
144
+ string: [
145
+ "network",
146
+ "org",
147
+ "region",
148
+ "image",
149
+ "config",
150
+ "main-port",
151
+ "template",
152
+ ],
153
+ boolean: ["help", "yes", "json"],
154
+ alias: { y: "yes" },
155
+ default: { "main-port": "80" },
156
+ };
157
+ const args = parseArgs(argv, opts);
158
+ checkArgs(args, opts, "ambit deploy");
159
+ if (args.help) {
160
+ console.log(`
161
+ ${bold("ambit deploy")} - Deploy an App Safely on a Custom Private Network
162
+
163
+ ${bold("USAGE")}
164
+ ambit deploy <app>.<network> [options]
165
+ ambit deploy <app> --network <name> [options]
166
+
167
+ The network can be specified as part of the name (app.network) or with --network.
168
+
169
+ ${bold("MODES")}
170
+ Config mode (default):
171
+ ambit deploy my-app.lab Uses ./fly.toml
172
+ ambit deploy my-app.lab --config path Explicit fly.toml
173
+
174
+ Image mode:
175
+ ambit deploy my-app.lab --image <img> Docker image, no toml
176
+
177
+ Template mode:
178
+ ambit deploy my-app.lab --template <ref> GitHub template
179
+
180
+ ${bold("OPTIONS")}
181
+ --network <name> Target network
182
+ --org <org> Fly.io organization slug
183
+ --region <region> Primary deployment region
184
+ -y, --yes Skip confirmation prompts
185
+ --json Output as JSON
186
+
187
+ ${bold("CONFIG MODE")} (default)
188
+ --config <path> Explicit fly.toml path (auto-detects ./fly.toml if omitted)
189
+
190
+ ${bold("IMAGE MODE")}
191
+ --image <img> Docker image to deploy (no fly.toml needed)
192
+ --main-port <port> Internal port for HTTP service (default: 80, "none" to skip)
193
+
194
+ ${bold("TEMPLATE MODE")}
195
+ --template <ref> GitHub template as owner/repo[/path][@ref]
196
+
197
+ Reference format:
198
+ owner/repo Fetch repo root from the default branch
199
+ owner/repo/path Fetch subdirectory from the default branch
200
+ owner/repo/path@tag Fetch a tagged release
201
+ owner/repo/path@branch Fetch a specific branch
202
+ owner/repo/path@commit Fetch a specific commit
203
+
204
+ ${bold("SAFETY")}
205
+ Always deploys with --no-public-ips and --flycast.
206
+ Post-deploy audit releases any public IPs and verifies Flycast allocation.
207
+ Pre-flight scan rejects fly.toml with force_https or TLS on 443.
208
+
209
+ ${bold("EXAMPLES")}
210
+ ambit deploy my-app.lab
211
+ ambit deploy my-app.lab --image registry/img:latest
212
+ ambit deploy my-app.lab --config ./fly.toml --region sea
213
+ ambit deploy my-claw.lab --template ToxicPine/ambit-openclaw
214
+ ambit deploy my-browser.lab --template ToxicPine/ambit-templates/chromatic
215
+ ambit deploy my-browser --network lab --template ToxicPine/ambit-templates/chromatic@v1.0
216
+ `);
217
+ return;
218
+ }
219
+ const out = createOutput(args.json);
220
+ const appArg = args._[0];
221
+ if (!appArg || typeof appArg !== "string") {
222
+ return out.die("Missing App Name. Usage: ambit deploy <app>.<network>");
223
+ }
224
+ let app;
225
+ let network;
226
+ if (appArg.includes(".")) {
227
+ const parts = appArg.split(".");
228
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
229
+ return out.die(`'${appArg}' Should Have Exactly One Dot, Like my-app.my-network`);
230
+ }
231
+ if (args.network) {
232
+ return out.die(`Network Is Already Part of the Name ('${appArg}'), --network Is Not Needed`);
233
+ }
234
+ app = parts[0];
235
+ network = parts[1];
236
+ }
237
+ else {
238
+ app = appArg;
239
+ if (!args.network) {
240
+ return out.die(`Missing Network. Use: ambit deploy ${app}.<network>`);
241
+ }
242
+ network = args.network;
243
+ }
244
+ try {
245
+ assertNotRouter(app);
246
+ }
247
+ catch (e) {
248
+ return out.die(e instanceof Error ? e.message : String(e));
249
+ }
250
+ const modeFlags = [args.image, args.config, args.template].filter(Boolean);
251
+ if (modeFlags.length > 1) {
252
+ return out.die("--image, --config, and --template Are Mutually Exclusive");
253
+ }
254
+ out.blank()
255
+ .header("=".repeat(50))
256
+ .header(` ambit Deploy: ${app}`)
257
+ .header("=".repeat(50))
258
+ .blank();
259
+ const { fly, org } = await stageFlyConfig(out, {
260
+ json: args.json,
261
+ org: args.org,
262
+ });
263
+ const { routerId, flyAppName, routerPrivateIp } = await stageNetworkVerification(out, fly, { org, network, app });
264
+ const deployConfig = await stagePreflightCheck(out, {
265
+ template: args.template,
266
+ image: args.image,
267
+ config: args.config,
268
+ mainPort: String(args["main-port"]),
269
+ });
270
+ await stageDeploy(out, fly, deployConfig, {
271
+ app,
272
+ network,
273
+ org,
274
+ region: args.region,
275
+ yes: args.yes,
276
+ json: args.json,
277
+ routerId,
278
+ flyAppName,
279
+ routerPrivateIp,
280
+ });
281
+ };
282
+ // =============================================================================
283
+ // Register Command
284
+ // =============================================================================
285
+ registerCommand({
286
+ name: "deploy",
287
+ description: "Deploy an app safely on a custom private network",
288
+ usage: "ambit deploy <app> --network <name> [--image <img>] [--template <ref>] [--org <org>] [--region <region>]",
289
+ run: deploy,
290
+ });
@@ -0,0 +1,52 @@
1
+ import { type Output } from "../../../lib/output.js";
2
+ import { Result } from "../../../lib/result.js";
3
+ import { type FlyProvider, type SafeDeployOptions } from "../../../providers/fly.js";
4
+ import type { DeployConfig } from "./modes.js";
5
+ export type DeployPhase = "create_app" | "set_proxy" | "deploy" | "audit" | "complete";
6
+ export type DeployResult = {
7
+ app: string;
8
+ network: string;
9
+ created: boolean;
10
+ audit: {
11
+ public_ips_released: number;
12
+ certs_removed: number;
13
+ flycast_allocations: Array<{
14
+ address: string;
15
+ network: string;
16
+ }>;
17
+ warnings: string[];
18
+ };
19
+ preflight: {
20
+ scanned: boolean;
21
+ warnings: string[];
22
+ };
23
+ };
24
+ export interface DeployCtx {
25
+ fly: FlyProvider;
26
+ out: Output<DeployResult>;
27
+ app: string;
28
+ network: string;
29
+ org: string;
30
+ region?: string;
31
+ yes: boolean;
32
+ json: boolean;
33
+ routerId: string;
34
+ flyAppName: string;
35
+ routerPrivateIp?: string;
36
+ created: boolean;
37
+ deployConfig: DeployConfig;
38
+ deployOptions: SafeDeployOptions;
39
+ audit?: {
40
+ public_ips_released: number;
41
+ certs_removed: number;
42
+ flycast_allocations: Array<{
43
+ address: string;
44
+ network: string;
45
+ }>;
46
+ warnings: string[];
47
+ };
48
+ }
49
+ export declare const reportDeploySkipped: (out: Output<DeployResult>, startPhase: DeployPhase) => void;
50
+ export declare const hydrateDeploy: (ctx: DeployCtx) => Promise<DeployPhase>;
51
+ export declare const deployTransition: (phase: DeployPhase, ctx: DeployCtx) => Promise<Result<DeployPhase>>;
52
+ //# sourceMappingURL=machine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/machine.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,OAAO,GACP,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE;QACL,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,SAAS,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,iBAAiB,CAAC;IACjC,KAAK,CAAC,EAAE;QACN,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH;AAaD,eAAO,MAAM,mBAAmB,GAC9B,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CAMrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAyG7B,CAAC"}
@@ -0,0 +1,116 @@
1
+ // =============================================================================
2
+ // Deploy — Phases, Context, Hydration, Transitions
3
+ // =============================================================================
4
+ import * as dntShim from "../../../_dnt.shims.js";
5
+ import { confirm } from "../../../lib/cli.js";
6
+ import { SECRET_AMBIT_OUTBOUND_PROXY, SOCKS_PROXY_PORT, } from "../../../util/constants.js";
7
+ import { Result } from "../../../lib/result.js";
8
+ import { FlyDeployError, } from "../../../providers/fly.js";
9
+ import { auditDeploy } from "../../../util/guard.js";
10
+ // =============================================================================
11
+ // Phase Labels
12
+ // =============================================================================
13
+ const DEPLOY_PHASES = [
14
+ { phase: "create_app", label: "App Created" },
15
+ { phase: "set_proxy", label: "Outbound Proxy Set" },
16
+ { phase: "deploy", label: "Deployed" },
17
+ { phase: "audit", label: "Audit Passed" },
18
+ ];
19
+ export const reportDeploySkipped = (out, startPhase) => {
20
+ for (const { phase, label } of DEPLOY_PHASES) {
21
+ if (phase === startPhase)
22
+ break;
23
+ out.skip(label);
24
+ }
25
+ };
26
+ // =============================================================================
27
+ // Hydration
28
+ // =============================================================================
29
+ export const hydrateDeploy = async (ctx) => {
30
+ const exists = await ctx.fly.apps.exists(ctx.flyAppName);
31
+ if (!exists)
32
+ return "create_app";
33
+ // App exists — always re-run set_proxy, deploy, and audit (they're idempotent)
34
+ return "set_proxy";
35
+ };
36
+ // =============================================================================
37
+ // Transitions
38
+ // =============================================================================
39
+ export const deployTransition = async (phase, ctx) => {
40
+ switch (phase) {
41
+ case "create_app": {
42
+ ctx.out.info(`App '${ctx.flyAppName}' Does Not Exist — Will Create on Network '${ctx.network}'`);
43
+ if (!ctx.yes && !ctx.json) {
44
+ const confirmed = await confirm(`Create App '${ctx.flyAppName}' on Network '${ctx.network}'?`);
45
+ if (!confirmed) {
46
+ ctx.out.text("Cancelled.");
47
+ return Result.err("Cancelled");
48
+ }
49
+ }
50
+ await ctx.out.spin("Creating App", () => ctx.fly.apps.create(ctx.app, ctx.org, {
51
+ network: ctx.network,
52
+ routerId: ctx.routerId,
53
+ }));
54
+ ctx.out.ok(`Created App '${ctx.flyAppName}' on Network '${ctx.network}'`);
55
+ ctx.created = true;
56
+ return Result.ok("set_proxy");
57
+ }
58
+ case "set_proxy": {
59
+ if (!ctx.created) {
60
+ ctx.out.skip(`App '${ctx.flyAppName}' Exists`);
61
+ }
62
+ if (ctx.routerPrivateIp) {
63
+ const proxyUrl = `socks5://[${ctx.routerPrivateIp}]:${SOCKS_PROXY_PORT}`;
64
+ await ctx.out.spin("Setting Outbound Proxy", () => ctx.fly.secrets.set(ctx.flyAppName, { [SECRET_AMBIT_OUTBOUND_PROXY]: proxyUrl }, { stage: true }));
65
+ ctx.out.ok(`Outbound Proxy: ${proxyUrl}`);
66
+ }
67
+ return Result.ok("deploy");
68
+ }
69
+ case "deploy": {
70
+ ctx.out.blank().dim("Deploying with --no-public-ips --flycast ...");
71
+ try {
72
+ await ctx.fly.deploy.app(ctx.app, ctx.deployOptions);
73
+ }
74
+ catch (e) {
75
+ if (e instanceof FlyDeployError) {
76
+ ctx.out.dim(` ${e.detail}`);
77
+ return Result.err(e.message);
78
+ }
79
+ throw e;
80
+ }
81
+ finally {
82
+ if (ctx.deployConfig.tempDir) {
83
+ try {
84
+ dntShim.Deno.removeSync(ctx.deployConfig.tempDir, { recursive: true });
85
+ }
86
+ catch {
87
+ /* ignore */
88
+ }
89
+ }
90
+ }
91
+ ctx.out.ok("Deploy Succeeded");
92
+ return Result.ok("audit");
93
+ }
94
+ case "audit": {
95
+ ctx.out.blank();
96
+ const auditSpinner = ctx.out.spinner("Auditing Deployment");
97
+ ctx.audit = await auditDeploy(ctx.fly, ctx.flyAppName, ctx.network);
98
+ auditSpinner.success("Audit Complete");
99
+ if (ctx.audit.public_ips_released > 0) {
100
+ ctx.out.warn(`Released ${ctx.audit.public_ips_released} Public IP(s)`);
101
+ }
102
+ if (ctx.audit.certs_removed > 0) {
103
+ ctx.out.ok(`Removed ${ctx.audit.certs_removed} Public Certificate(s)`);
104
+ }
105
+ for (const alloc of ctx.audit.flycast_allocations) {
106
+ ctx.out.ok(`Flycast: ${alloc.address} (network: ${alloc.network})`);
107
+ }
108
+ for (const warn of ctx.audit.warnings) {
109
+ ctx.out.warn(warn);
110
+ }
111
+ return Result.ok("complete");
112
+ }
113
+ default:
114
+ return Result.err(`Unknown Phase: ${phase}`);
115
+ }
116
+ };
@@ -0,0 +1,18 @@
1
+ import { createOutput } from "../../../lib/output.js";
2
+ /** Resolved deploy configuration — the output of mode-specific validation. */
3
+ export interface DeployConfig {
4
+ image?: string;
5
+ configPath?: string;
6
+ preflight: {
7
+ scanned: boolean;
8
+ warnings: string[];
9
+ };
10
+ tempDir?: string;
11
+ }
12
+ /** Resolve deploy config for image mode (--image). */
13
+ export declare const resolveImageMode: (image: string, mainPortRaw: string, out: ReturnType<typeof createOutput>) => DeployConfig | null;
14
+ /** Resolve deploy config for config mode (default — uses fly.toml). */
15
+ export declare const resolveConfigMode: (explicitConfig: string | undefined, out: ReturnType<typeof createOutput>) => Promise<DeployConfig | null>;
16
+ /** Resolve deploy config for template mode (--template). */
17
+ export declare const resolveTemplateMode: (templateRaw: string, out: ReturnType<typeof createOutput>) => Promise<DeployConfig | null>;
18
+ //# sourceMappingURL=modes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modes.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/modes.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAQtD,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAoCD,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,GAC3B,OAAO,MAAM,EACb,aAAa,MAAM,EACnB,KAAK,UAAU,CAAC,OAAO,YAAY,CAAC,KACnC,YAAY,GAAG,IAmBjB,CAAC;AAMF,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,GAC5B,gBAAgB,MAAM,GAAG,SAAS,EAClC,KAAK,UAAU,CAAC,OAAO,YAAY,CAAC,KACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAqC7B,CAAC;AAMF,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,GAC9B,aAAa,MAAM,EACnB,KAAK,UAAU,CAAC,OAAO,YAAY,CAAC,KACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAyE7B,CAAC"}