@mesh-tech/mesh-cli 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/{mesh.auth-preflight-JTUNS42O.js → mesh.auth-preflight-3ASNYA3I.js} +1 -1
- package/dist/bin/{mesh.chunk-QE5LUIHN.js → mesh.chunk-252LTIHZ.js} +2 -2
- package/dist/bin/{mesh.chunk-TYM27CO3.js → mesh.chunk-4Y3JGAYZ.js} +1 -1
- package/dist/bin/{mesh.chunk-OQA2UBOD.js → mesh.chunk-6OGUV2DU.js} +1 -1
- package/dist/bin/{mesh.chunk-XOCERV6F.js → mesh.chunk-G4TCVQLL.js} +556 -110
- package/dist/bin/mesh.chunk-G4TCVQLL.js.map +6 -0
- package/dist/bin/{mesh.chunk-ZNM3DDAM.js → mesh.chunk-HPLMVIBO.js} +1 -1
- package/dist/bin/{mesh.chunk-QUKWWFZI.js → mesh.chunk-JSY6THRF.js} +1 -1
- package/dist/bin/{mesh.chunk-PANTMVC3.js → mesh.chunk-NN3LTNUM.js} +3 -3
- package/dist/bin/{mesh.chunk-HB2IZKLP.js → mesh.chunk-SFIJZF3S.js} +786 -276
- package/dist/bin/{mesh.chunk-HB2IZKLP.js.map → mesh.chunk-SFIJZF3S.js.map} +3 -3
- package/dist/bin/{mesh.chunk-HC7V75J3.js → mesh.chunk-UJAPUO2W.js} +1 -1
- package/dist/bin/{mesh.create-app-DVMZDEQP.js → mesh.create-app-BHB3BGH7.js} +3 -3
- package/dist/bin/{mesh.dev-token-server-4N6HGQAF.js → mesh.dev-token-server-6TNWX5TY.js} +2 -2
- package/dist/bin/mesh.js +9 -9
- package/dist/bin/{mesh.login-Q6R5GMVM.js → mesh.login-4O5PMS25.js} +2 -2
- package/dist/bin/{mesh.platform-setup-YK6KSOP7.js → mesh.platform-setup-MTY6ICAO.js} +1 -1
- package/dist/bin/{mesh.program-NOBVA3JS.js → mesh.program-CMOLQWZI.js} +9 -9
- package/dist/bin/{mesh.registry-KA3CNNPZ.js → mesh.registry-BDBH6DRP.js} +4 -4
- package/dist/bin/{mesh.registry-identity-DSTAHXUS.js → mesh.registry-identity-QOGDEMZO.js} +3 -3
- package/dist/bin/{mesh.review-command-3ZXPS6Y3.js → mesh.review-command-E7V2L5AM.js} +3 -3
- package/dist/bin/{mesh.start-JDSYIRZI.js → mesh.start-3MUKXN2U.js} +2 -2
- package/dist/bin/{mesh.wizard-QGWPV4XN.js → mesh.wizard-EH3SQEHW.js} +7 -7
- package/dist/build-info.json +2 -2
- package/dist/src/commands/dev.d.ts +1 -0
- package/dist/src/commands/dev.js +57 -1
- package/dist/src/commands/temporal.js +146 -42
- package/dist/src/commands/temporal.js.map +3 -3
- package/dist/src/commands/workstation.d.ts +3 -0
- package/dist/src/commands/workstation.js +395 -0
- package/dist/src/program.js +2 -0
- package/dist/src/utils/temporal-inspect.d.ts +17 -0
- package/dist/src/utils/temporal-inspect.js +71 -0
- package/dist/src/utils/workstation-machine.d.ts +54 -0
- package/dist/src/utils/workstation-machine.js +282 -0
- package/dist/src/utils/workstation-routes.d.ts +77 -0
- package/dist/src/utils/workstation-routes.js +180 -0
- package/fragments/base/README.md.hbs +3 -0
- package/package.json +2 -2
- package/skills/mesh/SKILL.md +3 -2
- package/skills/mesh/references/worktrees.md +4 -1
- package/dist/bin/mesh.chunk-XOCERV6F.js.map +0 -6
- /package/dist/bin/{mesh.auth-preflight-JTUNS42O.js.map → mesh.auth-preflight-3ASNYA3I.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-QE5LUIHN.js.map → mesh.chunk-252LTIHZ.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-TYM27CO3.js.map → mesh.chunk-4Y3JGAYZ.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-OQA2UBOD.js.map → mesh.chunk-6OGUV2DU.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-ZNM3DDAM.js.map → mesh.chunk-HPLMVIBO.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-QUKWWFZI.js.map → mesh.chunk-JSY6THRF.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-PANTMVC3.js.map → mesh.chunk-NN3LTNUM.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-HC7V75J3.js.map → mesh.chunk-UJAPUO2W.js.map} +0 -0
- /package/dist/bin/{mesh.create-app-DVMZDEQP.js.map → mesh.create-app-BHB3BGH7.js.map} +0 -0
- /package/dist/bin/{mesh.dev-token-server-4N6HGQAF.js.map → mesh.dev-token-server-6TNWX5TY.js.map} +0 -0
- /package/dist/bin/{mesh.login-Q6R5GMVM.js.map → mesh.login-4O5PMS25.js.map} +0 -0
- /package/dist/bin/{mesh.platform-setup-YK6KSOP7.js.map → mesh.platform-setup-MTY6ICAO.js.map} +0 -0
- /package/dist/bin/{mesh.program-NOBVA3JS.js.map → mesh.program-CMOLQWZI.js.map} +0 -0
- /package/dist/bin/{mesh.registry-KA3CNNPZ.js.map → mesh.registry-BDBH6DRP.js.map} +0 -0
- /package/dist/bin/{mesh.registry-identity-DSTAHXUS.js.map → mesh.registry-identity-QOGDEMZO.js.map} +0 -0
- /package/dist/bin/{mesh.review-command-3ZXPS6Y3.js.map → mesh.review-command-E7V2L5AM.js.map} +0 -0
- /package/dist/bin/{mesh.start-JDSYIRZI.js.map → mesh.start-3MUKXN2U.js.map} +0 -0
- /package/dist/bin/{mesh.wizard-QGWPV4XN.js.map → mesh.wizard-EH3SQEHW.js.map} +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { execFileSync, spawn, spawnSync } from "child_process";
|
|
2
|
+
import { promises as dns } from "dns";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { logError, logInfo, logSuccess, logWarn } from "../utils/log.js";
|
|
7
|
+
import { listRoutes, readWorkstationIdentity, renderWorkstationCaddyfile, workstationDir, } from "../utils/workstation-routes.js";
|
|
8
|
+
import { renderCredentialProcessProfile, upsertManagedAwsConfigSection } from "../utils/aws-auth.js";
|
|
9
|
+
import { LAUNCH_AGENT_LABEL, LAUNCH_AGENTS_MACOS_ONLY, launchAgentsSupported, caddyDownloadUrl, caddyHasRoute53, certificateExpiry, credentialProcessProblem, discoveryRecordName, isInsideGitTree, zoneCandidates, readCredentialProcess, renderLaunchAgent, launchAgentSearchPath, parseDiscoveryRecord, rewriteCredentialProgram, shimFallsBackToGlobal, workstationWarnings, awsConfigFileOf, profileYieldsCredentials, proxyEnv, } from "../utils/workstation-machine.js";
|
|
10
|
+
export { proxyEnv } from "../utils/workstation-machine.js";
|
|
11
|
+
function tmuxSessionAlive(name) {
|
|
12
|
+
try {
|
|
13
|
+
execFileSync("tmux", ["has-session", "-t", `=${name}`], { stdio: "ignore" });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function profileProblem(proxy) {
|
|
21
|
+
if (!proxy.awsProfile)
|
|
22
|
+
return undefined;
|
|
23
|
+
let text;
|
|
24
|
+
try {
|
|
25
|
+
text = fs.readFileSync(awsConfigFileOf(proxy), "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return `cannot read ${awsConfigFileOf(proxy)}: ${e.message}`;
|
|
29
|
+
}
|
|
30
|
+
return credentialProcessProblem(readCredentialProcess(text, proxy.awsProfile), {
|
|
31
|
+
exists: (p) => fs.existsSync(p),
|
|
32
|
+
isInsideGitTree,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function adminPortInUse() {
|
|
36
|
+
try {
|
|
37
|
+
execFileSync("lsof", ["-nP", "-iTCP@127.0.0.1:2119", "-sTCP:LISTEN"], { stdio: "ignore" });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function launchAgentPath() {
|
|
45
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`);
|
|
46
|
+
}
|
|
47
|
+
function launchctl(args) {
|
|
48
|
+
return launchctlRun(args).ok;
|
|
49
|
+
}
|
|
50
|
+
function launchctlRun(args) {
|
|
51
|
+
const r = spawnSync("launchctl", args, { encoding: "utf8" });
|
|
52
|
+
return { ok: r.status === 0, output: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
53
|
+
}
|
|
54
|
+
function sleepMs(ms) {
|
|
55
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
56
|
+
}
|
|
57
|
+
function reloadLaunchAgent(domain, plist) {
|
|
58
|
+
const service = `${domain}/${LAUNCH_AGENT_LABEL}`;
|
|
59
|
+
launchctl(["bootout", service]);
|
|
60
|
+
for (let i = 0; i < 50 && launchctl(["print", service]); i++)
|
|
61
|
+
sleepMs(100);
|
|
62
|
+
let last = { ok: false, output: "" };
|
|
63
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
64
|
+
last = launchctlRun(["bootstrap", domain, plist]);
|
|
65
|
+
if (last.ok)
|
|
66
|
+
return last;
|
|
67
|
+
sleepMs(500);
|
|
68
|
+
}
|
|
69
|
+
return last;
|
|
70
|
+
}
|
|
71
|
+
const WORKSTATION_PROFILE = "mesh-workstation";
|
|
72
|
+
function fail(message) {
|
|
73
|
+
logError(message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
async function lookupTxt(resolver, record) {
|
|
77
|
+
try {
|
|
78
|
+
return (await resolver.resolveTxt(record)).map((chunks) => chunks.join(""));
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return e.code ?? e.message;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function lookupTxtAtZone(platform, record) {
|
|
85
|
+
for (const zone of zoneCandidates(platform)) {
|
|
86
|
+
let servers;
|
|
87
|
+
try {
|
|
88
|
+
servers = await dns.resolveNs(zone);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const ips = (await Promise.all(servers.map((ns) => dns.resolve4(ns).catch(() => [])))).flat();
|
|
94
|
+
if (ips.length === 0)
|
|
95
|
+
continue;
|
|
96
|
+
const resolver = new dns.Resolver();
|
|
97
|
+
resolver.setServers(ips);
|
|
98
|
+
return lookupTxt(resolver, record);
|
|
99
|
+
}
|
|
100
|
+
return "no name servers found";
|
|
101
|
+
}
|
|
102
|
+
function firstDiscovery(values) {
|
|
103
|
+
if (typeof values === "string")
|
|
104
|
+
return undefined;
|
|
105
|
+
return values.map((v) => parseDiscoveryRecord(v)).find((p) => typeof p !== "string");
|
|
106
|
+
}
|
|
107
|
+
async function discoverWorkstation(name, platform) {
|
|
108
|
+
const record = discoveryRecordName(name, platform);
|
|
109
|
+
const viaResolver = await lookupTxt(dns, record);
|
|
110
|
+
const found = firstDiscovery(viaResolver);
|
|
111
|
+
if (found)
|
|
112
|
+
return found;
|
|
113
|
+
const viaZone = await lookupTxtAtZone(platform, record);
|
|
114
|
+
const direct = firstDiscovery(viaZone);
|
|
115
|
+
if (direct) {
|
|
116
|
+
logInfo(`Your resolver has not seen ${record} yet; the zone's name servers answered`);
|
|
117
|
+
return direct;
|
|
118
|
+
}
|
|
119
|
+
const why = typeof viaZone === "string" ? viaZone : "no v=1 record";
|
|
120
|
+
fail(`No discovery record ${record} (${why}): is "${name}" in the platform's workstation stack, and deployed?`);
|
|
121
|
+
}
|
|
122
|
+
function requireFallbackShim(cli) {
|
|
123
|
+
let text;
|
|
124
|
+
try {
|
|
125
|
+
text = fs.readFileSync(cli, "utf8");
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
fail(`No mesh shim at ${cli}. Install one: npm i -g @mesh-tech/mesh-cli && mesh install-shim (or pass --cli <path>)`);
|
|
129
|
+
}
|
|
130
|
+
if (!shimFallsBackToGlobal(text)) {
|
|
131
|
+
fail(`${cli} is an older shim that fails outside a workspace, where renewal runs. Update it: mesh install-shim --force`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function ensureCaddy(dir) {
|
|
135
|
+
const caddy = path.join(dir, "bin", "caddy");
|
|
136
|
+
const hasRoute53 = (bin) => {
|
|
137
|
+
try {
|
|
138
|
+
return caddyHasRoute53(execFileSync(bin, ["list-modules"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
if (fs.existsSync(caddy) && hasRoute53(caddy))
|
|
145
|
+
return caddy;
|
|
146
|
+
const url = caddyDownloadUrl(process.platform, process.arch);
|
|
147
|
+
logInfo(`Downloading Caddy with the Route 53 module (${url})...`);
|
|
148
|
+
const resp = await fetch(url);
|
|
149
|
+
if (!resp.ok)
|
|
150
|
+
fail(`Caddy download failed: HTTP ${resp.status}. Build it instead (xcaddy build --with github.com/caddy-dns/route53) and pass --caddy`);
|
|
151
|
+
fs.mkdirSync(path.dirname(caddy), { recursive: true });
|
|
152
|
+
const tmp = `${caddy}.download`;
|
|
153
|
+
fs.writeFileSync(tmp, Buffer.from(await resp.arrayBuffer()), { mode: 0o755 });
|
|
154
|
+
if (!hasRoute53(tmp)) {
|
|
155
|
+
fs.rmSync(tmp, { force: true });
|
|
156
|
+
fail("The downloaded Caddy has no dns.providers.route53 module");
|
|
157
|
+
}
|
|
158
|
+
fs.renameSync(tmp, caddy);
|
|
159
|
+
logSuccess(`Caddy: ${caddy}`);
|
|
160
|
+
return caddy;
|
|
161
|
+
}
|
|
162
|
+
function writeCaddyfile(dir, identity) {
|
|
163
|
+
const caddyfile = path.join(dir, "Caddyfile");
|
|
164
|
+
fs.mkdirSync(path.join(dir, "routes"), { recursive: true });
|
|
165
|
+
fs.writeFileSync(caddyfile, renderWorkstationCaddyfile(identity, dir));
|
|
166
|
+
return caddyfile;
|
|
167
|
+
}
|
|
168
|
+
function requireIdentity(dir) {
|
|
169
|
+
const identity = readWorkstationIdentity(dir);
|
|
170
|
+
if (!identity) {
|
|
171
|
+
logError(`This machine is not a workstation (no ${path.join(dir, "workstation.json")}). Run: mesh workstation init`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
return identity;
|
|
175
|
+
}
|
|
176
|
+
export function registerWorkstationCommands(program) {
|
|
177
|
+
const ws = program
|
|
178
|
+
.command("workstation")
|
|
179
|
+
.description("HTTPS names on this machine for the services it runs (dev servers, local stacks)");
|
|
180
|
+
ws.command("init")
|
|
181
|
+
.description("Make this machine a workstation: its name, certificate credentials and proxy binary")
|
|
182
|
+
.requiredOption("--name <label>", "this machine's host label in the workstation stack (e.g. mbp)")
|
|
183
|
+
.option("--platform <domain>", "the platform it is named under (e.g. dev.acme.example.com): the rest is discovered")
|
|
184
|
+
.option("--domain <domain>", "without --platform: <name>.<platform zone>")
|
|
185
|
+
.option("--zone-id <id>", "the Route 53 zone (default: discovered)")
|
|
186
|
+
.option("--region <region>", "AWS region for Route 53 (default: discovered, else us-east-2)")
|
|
187
|
+
.option("--aws-profile <profile>", "use this existing profile instead of writing one")
|
|
188
|
+
.option("--aws-config-file <file>", "the AWS config file holding --aws-profile")
|
|
189
|
+
.option("--caddy <path>", "Caddy built with github.com/caddy-dns/route53 (default: downloaded)")
|
|
190
|
+
.option("--cli <path>", "the mesh CLI the profile runs (default: the shim, ~/.local/bin/mesh)")
|
|
191
|
+
.option("--allow-checkout-cli", "accept a credential_process inside a git checkout (renewal breaks when it is removed)")
|
|
192
|
+
.option("--force", "overwrite an existing workstation.json")
|
|
193
|
+
.action(async (opts) => {
|
|
194
|
+
const dir = workstationDir();
|
|
195
|
+
const file = path.join(dir, "workstation.json");
|
|
196
|
+
if (fs.existsSync(file) && !opts.force)
|
|
197
|
+
fail(`${file} exists; --force to replace it`);
|
|
198
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
199
|
+
let domain;
|
|
200
|
+
let zoneId = opts.zoneId;
|
|
201
|
+
let region = opts.region;
|
|
202
|
+
let roleArn;
|
|
203
|
+
if (opts.platform) {
|
|
204
|
+
domain = `${opts.name}.${opts.platform.replace(/\.$/, "")}`;
|
|
205
|
+
if (opts.domain && opts.domain !== domain)
|
|
206
|
+
fail(`--domain ${opts.domain} is not ${domain} (--name under --platform)`);
|
|
207
|
+
const found = await discoverWorkstation(opts.name, opts.platform);
|
|
208
|
+
roleArn = found.roleArn;
|
|
209
|
+
zoneId ??= found.zoneId;
|
|
210
|
+
region ??= found.region;
|
|
211
|
+
logInfo(`Discovered ${domain}: zone ${found.zoneId}, role ${found.roleArn.split("/").pop()}`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
if (!opts.domain || !zoneId)
|
|
215
|
+
fail("Give --platform <domain> (everything else is discovered), or --domain and --zone-id");
|
|
216
|
+
domain = opts.domain;
|
|
217
|
+
}
|
|
218
|
+
if (!domain.startsWith(`${opts.name}.`))
|
|
219
|
+
fail(`--domain must be under --name: ${opts.name}.<zone>`);
|
|
220
|
+
region ??= "us-east-2";
|
|
221
|
+
const cli = path.resolve(opts.cli ?? path.join(os.homedir(), ".local", "bin", "mesh"));
|
|
222
|
+
let awsProfile = opts.awsProfile;
|
|
223
|
+
let awsConfigFile = opts.awsConfigFile ? path.resolve(opts.awsConfigFile) : undefined;
|
|
224
|
+
if (!awsProfile) {
|
|
225
|
+
if (!opts.platform || !roleArn)
|
|
226
|
+
fail("Without --platform, give --aws-profile (a mesh login --export profile)");
|
|
227
|
+
if (!opts.cli)
|
|
228
|
+
requireFallbackShim(cli);
|
|
229
|
+
awsProfile = WORKSTATION_PROFILE;
|
|
230
|
+
awsConfigFile = path.join(dir, "aws-config");
|
|
231
|
+
const profile = renderCredentialProcessProfile({ profileName: awsProfile, context: opts.platform, roleArn, region, meshBin: cli });
|
|
232
|
+
const existing = fs.existsSync(awsConfigFile) ? fs.readFileSync(awsConfigFile, "utf8") : "";
|
|
233
|
+
fs.writeFileSync(awsConfigFile, upsertManagedAwsConfigSection(existing, `workstation ${opts.platform}`, profile));
|
|
234
|
+
logSuccess(`${awsConfigFile}: [profile ${awsProfile}] runs ${cli}`);
|
|
235
|
+
}
|
|
236
|
+
else if (opts.cli) {
|
|
237
|
+
const configFile = awsConfigFile ?? path.join(os.homedir(), ".aws", "config");
|
|
238
|
+
fs.writeFileSync(configFile, rewriteCredentialProgram(fs.readFileSync(configFile, "utf8"), awsProfile, cli));
|
|
239
|
+
logSuccess(`${configFile}: [profile ${awsProfile}] now runs ${cli}`);
|
|
240
|
+
}
|
|
241
|
+
const caddy = opts.caddy ? path.resolve(opts.caddy) : await ensureCaddy(dir);
|
|
242
|
+
const identity = {
|
|
243
|
+
name: opts.name,
|
|
244
|
+
domain,
|
|
245
|
+
proxy: {
|
|
246
|
+
zoneId: zoneId,
|
|
247
|
+
region,
|
|
248
|
+
awsProfile,
|
|
249
|
+
...(awsConfigFile ? { awsConfigFile } : {}),
|
|
250
|
+
caddy,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const proxy = identity.proxy;
|
|
254
|
+
const problem = profileProblem(proxy);
|
|
255
|
+
if (problem && !opts.allowCheckoutCli) {
|
|
256
|
+
logError(`The profile ${awsProfile} will not keep renewing the certificate: ${problem}.`);
|
|
257
|
+
logInfo(" Point it at a mesh CLI outside any checkout: --cli <path>, or --allow-checkout-cli to accept it.");
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
if (problem)
|
|
261
|
+
logWarn(`Accepted (--allow-checkout-cli): ${problem}.`);
|
|
262
|
+
if (!profileYieldsCredentials(proxy)) {
|
|
263
|
+
if (!opts.platform)
|
|
264
|
+
fail(`The profile ${awsProfile} yields no AWS credentials yet: run its mesh login, then init again`);
|
|
265
|
+
logInfo(`Signing in to ${opts.platform} for the certificate role...`);
|
|
266
|
+
const login = spawnSync(process.execPath, [...process.execArgv, process.argv[1], "login", opts.platform], { stdio: "inherit" });
|
|
267
|
+
if (login.status !== 0 || !profileYieldsCredentials(proxy)) {
|
|
268
|
+
fail(`The profile ${awsProfile} still yields no credentials for ${roleArn}: is your Zitadel user one of the stack's subjects?`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
fs.mkdirSync(path.join(dir, "routes"), { recursive: true });
|
|
272
|
+
fs.writeFileSync(file, JSON.stringify(identity, null, 2) + "\n");
|
|
273
|
+
logSuccess(`Workstation ${opts.name}: https://*.${domain} (${file})`);
|
|
274
|
+
logInfo("Keep its proxy running with: mesh workstation proxy --install");
|
|
275
|
+
});
|
|
276
|
+
ws.command("proxy")
|
|
277
|
+
.description("Run the workstation's proxy (Caddy) in the foreground: certificate, routes, reloads")
|
|
278
|
+
.option("--print", "print the Caddyfile and the command instead of running")
|
|
279
|
+
.option("--install", "run it as a LaunchAgent (starts now, at login, and again if it exits)")
|
|
280
|
+
.option("--uninstall", "stop the LaunchAgent and remove it")
|
|
281
|
+
.action((opts) => {
|
|
282
|
+
const dir = workstationDir();
|
|
283
|
+
const domain = `gui/${os.userInfo().uid}`;
|
|
284
|
+
if ((opts.install || opts.uninstall) && !launchAgentsSupported()) {
|
|
285
|
+
logError(LAUNCH_AGENTS_MACOS_ONLY);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
if (opts.uninstall) {
|
|
289
|
+
launchctl(["bootout", `${domain}/${LAUNCH_AGENT_LABEL}`]);
|
|
290
|
+
fs.rmSync(launchAgentPath(), { force: true });
|
|
291
|
+
logSuccess(`Removed the ${LAUNCH_AGENT_LABEL} LaunchAgent`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const identity = requireIdentity(dir);
|
|
295
|
+
if (!identity.proxy) {
|
|
296
|
+
logError("workstation.json has no proxy settings. Run: mesh workstation init --force …");
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
const proxy = identity.proxy;
|
|
300
|
+
const caddyfile = writeCaddyfile(dir, { ...identity, proxy });
|
|
301
|
+
const caddy = proxy.caddy ?? "caddy";
|
|
302
|
+
const args = ["run", "--watch", "--config", caddyfile, "--adapter", "caddyfile"];
|
|
303
|
+
if (opts.print) {
|
|
304
|
+
console.log(fs.readFileSync(caddyfile, "utf8"));
|
|
305
|
+
console.log(`${caddy} ${args.join(" ")}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (opts.install) {
|
|
309
|
+
if (!launchctl(["print", `${domain}/${LAUNCH_AGENT_LABEL}`]) && adminPortInUse()) {
|
|
310
|
+
logError("Another workstation proxy is running (127.0.0.1:2119 answers). Stop it, then run --install again.");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
const problem = profileProblem(proxy);
|
|
314
|
+
if (problem)
|
|
315
|
+
logWarn(`The profile will not keep renewing the certificate: ${problem}.`);
|
|
316
|
+
const env = proxyEnv({ PATH: launchAgentSearchPath(process.env.PATH, process.execPath), HOME: os.homedir() }, proxy);
|
|
317
|
+
const plist = renderLaunchAgent({ caddy, caddyfile, dir, env: env });
|
|
318
|
+
fs.mkdirSync(path.dirname(launchAgentPath()), { recursive: true });
|
|
319
|
+
fs.writeFileSync(launchAgentPath(), plist);
|
|
320
|
+
const loaded = reloadLaunchAgent(domain, launchAgentPath());
|
|
321
|
+
if (!loaded.ok) {
|
|
322
|
+
logError(`launchctl bootstrap failed for ${launchAgentPath()}${loaded.output ? `: ${loaded.output}` : ""}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
logSuccess(`LaunchAgent ${LAUNCH_AGENT_LABEL}: serving https://*.${identity.domain} (log: ${path.join(dir, "proxy.log")})`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
logInfo(`Serving https://*.${identity.domain} — routes in ${path.join(dir, "routes")}`);
|
|
329
|
+
const child = spawn(caddy, args, { stdio: "inherit", env: proxyEnv(process.env, identity.proxy) });
|
|
330
|
+
child.on("error", (e) => {
|
|
331
|
+
logError(`Could not start ${caddy}: ${e.message}. Build it with: xcaddy build --with github.com/caddy-dns/route53`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
});
|
|
334
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
335
|
+
});
|
|
336
|
+
ws.command("routes")
|
|
337
|
+
.description("List the HTTPS names this workstation serves")
|
|
338
|
+
.option("--prune", "remove routes of mesh dev sessions that are no longer running")
|
|
339
|
+
.option("--json", "print JSON")
|
|
340
|
+
.action((opts) => {
|
|
341
|
+
const dir = workstationDir();
|
|
342
|
+
requireIdentity(dir);
|
|
343
|
+
let routes = listRoutes(dir).map((r) => ({
|
|
344
|
+
...r,
|
|
345
|
+
alive: r.session ? tmuxSessionAlive(r.session) : undefined,
|
|
346
|
+
}));
|
|
347
|
+
if (opts.prune) {
|
|
348
|
+
for (const r of routes.filter((r) => r.session && !r.alive)) {
|
|
349
|
+
fs.rmSync(r.file, { force: true });
|
|
350
|
+
logInfo(`Removed ${r.hostname} (session ${r.session} is not running)`);
|
|
351
|
+
}
|
|
352
|
+
routes = routes.filter((r) => !r.session || r.alive);
|
|
353
|
+
}
|
|
354
|
+
if (opts.json) {
|
|
355
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (routes.length === 0) {
|
|
359
|
+
logInfo("No routes.");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
for (const r of routes) {
|
|
363
|
+
const owner = r.session ? `mesh dev ${r.session}${r.alive ? "" : " (not running)"}` : "stack";
|
|
364
|
+
console.log(` https://${r.hostname.padEnd(56)} → :${String(r.port).padEnd(6)} ${owner}`);
|
|
365
|
+
}
|
|
366
|
+
if (routes.some((r) => r.session && !r.alive))
|
|
367
|
+
logWarn("Clear routes of stopped sessions with: mesh workstation routes --prune");
|
|
368
|
+
});
|
|
369
|
+
ws.command("status")
|
|
370
|
+
.description("Is this workstation healthy: proxy running, certificate fresh, renewal credentials alive")
|
|
371
|
+
.action(() => {
|
|
372
|
+
const dir = workstationDir();
|
|
373
|
+
const identity = requireIdentity(dir);
|
|
374
|
+
const expiry = certificateExpiry(dir, identity.domain);
|
|
375
|
+
const launchd = launchAgentsSupported();
|
|
376
|
+
const agent = launchd && launchctl(["print", `gui/${os.userInfo().uid}/${LAUNCH_AGENT_LABEL}`]);
|
|
377
|
+
const problem = identity.proxy ? profileProblem(identity.proxy) : undefined;
|
|
378
|
+
const credentialOk = identity.proxy ? profileYieldsCredentials(identity.proxy) : true;
|
|
379
|
+
console.log(` workstation ${identity.name} (*.${identity.domain})`);
|
|
380
|
+
const proxyLine = !launchd
|
|
381
|
+
? LAUNCH_AGENTS_MACOS_ONLY
|
|
382
|
+
: agent
|
|
383
|
+
? `LaunchAgent ${LAUNCH_AGENT_LABEL}`
|
|
384
|
+
: "no LaunchAgent (mesh workstation proxy --install)";
|
|
385
|
+
console.log(` proxy ${proxyLine}`);
|
|
386
|
+
console.log(` certificate ${expiry ? `expires ${expiry.toISOString().slice(0, 10)}` : "none issued yet"}`);
|
|
387
|
+
console.log(` credentials ${credentialOk ? "ok" : "FAILING"}${identity.proxy?.awsProfile ? ` (profile ${identity.proxy.awsProfile})` : ""}`);
|
|
388
|
+
console.log(` routes ${listRoutes(dir).length}`);
|
|
389
|
+
const warnings = workstationWarnings({ now: new Date(), certExpiry: expiry, credentialOk, credentialProblem: problem });
|
|
390
|
+
for (const w of warnings)
|
|
391
|
+
logWarn(w);
|
|
392
|
+
if (warnings.length > 0)
|
|
393
|
+
process.exitCode = 1;
|
|
394
|
+
});
|
|
395
|
+
}
|
package/dist/src/program.js
CHANGED
|
@@ -25,6 +25,7 @@ import { registerTunnelCommands } from "./commands/tunnel/index.js";
|
|
|
25
25
|
import { registerVcsCommands } from "./commands/vcs/index.js";
|
|
26
26
|
import { registerVpnCommands } from "./commands/vpn/index.js";
|
|
27
27
|
import { registerWorkflowCommands } from "./commands/workflow.js";
|
|
28
|
+
import { registerWorkstationCommands } from "./commands/workstation.js";
|
|
28
29
|
export const CLI_NAME = "mesh";
|
|
29
30
|
export const CLI_DESCRIPTION = "Build, run and deploy apps on a Mesh platform";
|
|
30
31
|
export function createProgram(options = {}) {
|
|
@@ -58,5 +59,6 @@ export function createProgram(options = {}) {
|
|
|
58
59
|
registerInitCommands(program);
|
|
59
60
|
registerTenantCommands(program);
|
|
60
61
|
registerInstallShimCommand(program);
|
|
62
|
+
registerWorkstationCommands(program);
|
|
61
63
|
return program;
|
|
62
64
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function enumMemberName(member: string, prefix: string): string;
|
|
2
|
+
export declare function eventTypeNames(enumObject?: Record<string, unknown>): Record<number, string>;
|
|
3
|
+
export declare const LIST_STATUSES: Record<string, string>;
|
|
4
|
+
export declare function buildListQuery(opts: {
|
|
5
|
+
type?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
parent?: string;
|
|
8
|
+
query?: string;
|
|
9
|
+
}): string;
|
|
10
|
+
export type ListedExecution = {
|
|
11
|
+
workflowId: string;
|
|
12
|
+
type: string;
|
|
13
|
+
status: string;
|
|
14
|
+
startTime?: Date;
|
|
15
|
+
parentWorkflowId?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function formatListRows(rows: ListedExecution[]): string[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
export function enumMemberName(member, prefix) {
|
|
3
|
+
return member
|
|
4
|
+
.slice(prefix.length)
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.split("_")
|
|
7
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
8
|
+
.join("");
|
|
9
|
+
}
|
|
10
|
+
export function eventTypeNames(enumObject = loadEventTypeEnum()) {
|
|
11
|
+
const names = {};
|
|
12
|
+
for (const [member, value] of Object.entries(enumObject)) {
|
|
13
|
+
if (typeof value === "number" && member.startsWith("EVENT_TYPE_") && value !== 0) {
|
|
14
|
+
names[value] = enumMemberName(member, "EVENT_TYPE_");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return names;
|
|
18
|
+
}
|
|
19
|
+
function loadEventTypeEnum() {
|
|
20
|
+
try {
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const fromClient = createRequire(require.resolve("@temporalio/client"));
|
|
23
|
+
const proto = fromClient("@temporalio/proto");
|
|
24
|
+
return proto.temporal.api.enums.v1.EventType;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const LIST_STATUSES = {
|
|
31
|
+
running: "Running",
|
|
32
|
+
completed: "Completed",
|
|
33
|
+
failed: "Failed",
|
|
34
|
+
canceled: "Canceled",
|
|
35
|
+
terminated: "Terminated",
|
|
36
|
+
"continued-as-new": "ContinuedAsNew",
|
|
37
|
+
"timed-out": "TimedOut",
|
|
38
|
+
};
|
|
39
|
+
function quote(value) {
|
|
40
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
41
|
+
}
|
|
42
|
+
export function buildListQuery(opts) {
|
|
43
|
+
const clauses = [];
|
|
44
|
+
if (opts.type)
|
|
45
|
+
clauses.push(`WorkflowType = ${quote(opts.type)}`);
|
|
46
|
+
if (opts.status) {
|
|
47
|
+
const status = LIST_STATUSES[opts.status.toLowerCase()];
|
|
48
|
+
if (!status)
|
|
49
|
+
throw new Error(`--status must be one of: ${Object.keys(LIST_STATUSES).join(", ")}`);
|
|
50
|
+
clauses.push(`ExecutionStatus = ${quote(status)}`);
|
|
51
|
+
}
|
|
52
|
+
if (opts.parent)
|
|
53
|
+
clauses.push(`ParentWorkflowId = ${quote(opts.parent)}`);
|
|
54
|
+
if (opts.query)
|
|
55
|
+
clauses.push(clauses.length > 0 ? `(${opts.query})` : opts.query);
|
|
56
|
+
return clauses.join(" AND ");
|
|
57
|
+
}
|
|
58
|
+
export function formatListRows(rows) {
|
|
59
|
+
const table = [
|
|
60
|
+
["WORKFLOW ID", "TYPE", "STATUS", "STARTED", "PARENT"],
|
|
61
|
+
...rows.map((r) => [
|
|
62
|
+
r.workflowId,
|
|
63
|
+
r.type,
|
|
64
|
+
r.status,
|
|
65
|
+
r.startTime ? r.startTime.toISOString().replace(/\.\d{3}Z$/, "Z") : "-",
|
|
66
|
+
r.parentWorkflowId ?? "-",
|
|
67
|
+
]),
|
|
68
|
+
];
|
|
69
|
+
const widths = table[0].map((_, i) => Math.max(...table.map((row) => row[i].length)));
|
|
70
|
+
return table.map((row) => row.map((cell, i) => (i === row.length - 1 ? cell : cell.padEnd(widths[i]))).join(" "));
|
|
71
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type WorkstationProxyConfig } from "./workstation-routes.js";
|
|
2
|
+
export declare function readCredentialProcess(configText: string, profile: string): string | undefined;
|
|
3
|
+
export declare function credentialProgram(command: string): string;
|
|
4
|
+
export declare function credentialProcessProblem(command: string | undefined, deps: {
|
|
5
|
+
exists: (p: string) => boolean;
|
|
6
|
+
isInsideGitTree: (p: string) => boolean;
|
|
7
|
+
}): string | undefined;
|
|
8
|
+
export declare function isInsideGitTree(p: string): boolean;
|
|
9
|
+
export declare function rewriteCredentialProgram(configText: string, profile: string, cli: string): string;
|
|
10
|
+
export declare const LAUNCH_AGENT_LABEL = "tech.mesh.workstation-proxy";
|
|
11
|
+
export declare const LAUNCH_AGENTS_MACOS_ONLY = "LaunchAgents are macOS-only; run mesh workstation proxy in the foreground (or under your own service manager)";
|
|
12
|
+
export declare function launchAgentsSupported(platform?: NodeJS.Platform): boolean;
|
|
13
|
+
export declare function launchAgentSearchPath(installerPath: string | undefined, execPath: string): string;
|
|
14
|
+
export declare function renderLaunchAgent(args: {
|
|
15
|
+
caddy: string;
|
|
16
|
+
caddyfile: string;
|
|
17
|
+
dir: string;
|
|
18
|
+
env: Record<string, string>;
|
|
19
|
+
}): string;
|
|
20
|
+
export declare function certificateExpiry(dir: string, domain: string): Date | undefined;
|
|
21
|
+
export declare function workstationWarnings(args: {
|
|
22
|
+
now: Date;
|
|
23
|
+
certExpiry: Date | undefined;
|
|
24
|
+
credentialOk: boolean;
|
|
25
|
+
credentialProblem?: string;
|
|
26
|
+
}): string[];
|
|
27
|
+
export type WorkstationDiscovery = {
|
|
28
|
+
roleArn: string;
|
|
29
|
+
zoneId: string;
|
|
30
|
+
region: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function discoveryRecordName(name: string, platform: string): string;
|
|
33
|
+
export declare function parseDiscoveryRecord(text: string): WorkstationDiscovery | string;
|
|
34
|
+
export declare function caddyDownloadUrl(platform: string, arch: string): string;
|
|
35
|
+
export declare function caddyHasRoute53(listModulesOutput: string): boolean;
|
|
36
|
+
export declare function shimFallsBackToGlobal(shimText: string): boolean;
|
|
37
|
+
export declare function zoneCandidates(platform: string): string[];
|
|
38
|
+
export declare const RENEWAL_OVERDUE_DAYS = 28;
|
|
39
|
+
export type WorkstationHealthStamp = {
|
|
40
|
+
checkedAt: string;
|
|
41
|
+
credentialOk: boolean;
|
|
42
|
+
};
|
|
43
|
+
export declare function healthCheckDue(stamp: WorkstationHealthStamp | undefined, now: Date): boolean;
|
|
44
|
+
export declare function loginContextOf(command: string | undefined): string | undefined;
|
|
45
|
+
export declare function renewalWarnings(args: {
|
|
46
|
+
now: Date;
|
|
47
|
+
certExpiry: Date | undefined;
|
|
48
|
+
credentialOk: boolean;
|
|
49
|
+
loginContext?: string;
|
|
50
|
+
}): string[];
|
|
51
|
+
export declare function proxyEnv(base: NodeJS.ProcessEnv, proxy: WorkstationProxyConfig): NodeJS.ProcessEnv;
|
|
52
|
+
export declare function awsConfigFileOf(proxy: WorkstationProxyConfig): string;
|
|
53
|
+
export declare function profileYieldsCredentials(proxy: WorkstationProxyConfig, timeoutMs?: number): boolean;
|
|
54
|
+
export declare function workstationRenewalWarnings(now?: Date): string[];
|