create-op-node 0.10.11 → 0.10.13
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/cli.js +110 -43
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,18 +12,38 @@ import { readFileSync } from 'fs';
|
|
|
12
12
|
import * as tls from 'tls';
|
|
13
13
|
import { Validator } from '@cfworker/json-schema';
|
|
14
14
|
|
|
15
|
+
// src/lib/constants.ts
|
|
16
|
+
var PGSODIUM_KEY_RE = /^[a-f0-9]{64}$/;
|
|
17
|
+
var TUNNEL_TOKEN_RE = /^[A-Za-z0-9_\-.=]+$/;
|
|
18
|
+
var SAFE_PATH_RE = /^[A-Za-z0-9_\-./ ]+$/;
|
|
19
|
+
var SAFE_LAUNCHCTL_VALUE_RE = /^[A-Za-z0-9+/=._-]+$/;
|
|
20
|
+
var URL_SAFE_PASSWORD_RE = /^[A-Za-z0-9_-]+$/;
|
|
21
|
+
var SAFE_URL_RE = /^[A-Za-z0-9:/_.-]+$/;
|
|
22
|
+
var VERIFY_NETWORK_TIMEOUT_MS = 1e4;
|
|
23
|
+
var API_REQUEST_TIMEOUT_MS = 15e3;
|
|
24
|
+
var BODY_PREVIEW_MAX = 200;
|
|
25
|
+
|
|
15
26
|
// src/lib/cloudflare.ts
|
|
16
27
|
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
17
28
|
async function get(token, path, fetchImpl = fetch) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
let body = null;
|
|
29
|
+
const ctrl = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => ctrl.abort(), API_REQUEST_TIMEOUT_MS);
|
|
22
31
|
try {
|
|
23
|
-
|
|
32
|
+
const res = await fetchImpl(`${CF_API}${path}`, {
|
|
33
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
34
|
+
signal: ctrl.signal
|
|
35
|
+
});
|
|
36
|
+
let body = null;
|
|
37
|
+
try {
|
|
38
|
+
body = await res.json();
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
return { status: res.status, body };
|
|
24
42
|
} catch {
|
|
43
|
+
return { status: 0, body: null };
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
25
46
|
}
|
|
26
|
-
return { status: res.status, body };
|
|
27
47
|
}
|
|
28
48
|
function shortError(body) {
|
|
29
49
|
if (typeof body === "object" && body !== null && "errors" in body && Array.isArray(body.errors)) {
|
|
@@ -38,6 +58,12 @@ function shortError(body) {
|
|
|
38
58
|
async function probeCloudflareToken(input) {
|
|
39
59
|
const issues = [];
|
|
40
60
|
const verify = await get(input.token, `/accounts/${input.accountId}/tokens/verify`);
|
|
61
|
+
if (verify.status === 0) {
|
|
62
|
+
issues.push(
|
|
63
|
+
"Couldn't reach Cloudflare (network error or timeout). Check your connection and retry."
|
|
64
|
+
);
|
|
65
|
+
return { ok: false, issues };
|
|
66
|
+
}
|
|
41
67
|
if (verify.status !== 200) {
|
|
42
68
|
issues.push(
|
|
43
69
|
`Token verify failed (HTTP ${verify.status}): ${shortError(verify.body)}`
|
|
@@ -55,7 +81,9 @@ async function probeCloudflareToken(input) {
|
|
|
55
81
|
const res = await get(input.token, check.path);
|
|
56
82
|
if (res.status === 200) continue;
|
|
57
83
|
if (res.status === 404 && check.treat404AsPass) continue;
|
|
58
|
-
if (res.status ===
|
|
84
|
+
if (res.status === 0) {
|
|
85
|
+
issues.push(`Couldn't reach Cloudflare while checking ${check.name} (network error or timeout).`);
|
|
86
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
59
87
|
issues.push(`Missing scope: ${check.name}`);
|
|
60
88
|
} else if (res.status === 400 && typeof res.body === "object" && res.body !== null && "errors" in res.body && Array.isArray(res.body.errors) && res.body.errors.some((e) => e.code === 10042)) {
|
|
61
89
|
issues.push(
|
|
@@ -75,6 +103,9 @@ async function tunnelStatus(input) {
|
|
|
75
103
|
`/accounts/${input.accountId}/cfd_tunnel/${input.tunnelId}`,
|
|
76
104
|
fetchImpl
|
|
77
105
|
);
|
|
106
|
+
if (res.status === 0) {
|
|
107
|
+
return { ok: false, reason: "couldn't reach Cloudflare (network error or timeout)" };
|
|
108
|
+
}
|
|
78
109
|
if (res.status !== 200) {
|
|
79
110
|
return {
|
|
80
111
|
ok: false,
|
|
@@ -103,13 +134,24 @@ function authHeaders(token) {
|
|
|
103
134
|
};
|
|
104
135
|
}
|
|
105
136
|
async function getJson(token, path) {
|
|
106
|
-
const
|
|
107
|
-
|
|
137
|
+
const ctrl = new AbortController();
|
|
138
|
+
const timer = setTimeout(() => ctrl.abort(), API_REQUEST_TIMEOUT_MS);
|
|
108
139
|
try {
|
|
109
|
-
|
|
140
|
+
const res = await fetch(`${TFC_API}${path}`, {
|
|
141
|
+
headers: authHeaders(token),
|
|
142
|
+
signal: ctrl.signal
|
|
143
|
+
});
|
|
144
|
+
let body = null;
|
|
145
|
+
try {
|
|
146
|
+
body = await res.json();
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
return { status: res.status, body };
|
|
110
150
|
} catch {
|
|
151
|
+
return { status: 0, body: null };
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(timer);
|
|
111
154
|
}
|
|
112
|
-
return { status: res.status, body };
|
|
113
155
|
}
|
|
114
156
|
async function probeTfcToken(input) {
|
|
115
157
|
const issues = [];
|
|
@@ -120,6 +162,12 @@ async function probeTfcToken(input) {
|
|
|
120
162
|
return { ok: false, issues };
|
|
121
163
|
}
|
|
122
164
|
const account = await getJson(input.token, "/account/details");
|
|
165
|
+
if (account.status === 0) {
|
|
166
|
+
issues.push(
|
|
167
|
+
"Couldn't reach Terraform Cloud (network error or timeout). Check your connection and retry."
|
|
168
|
+
);
|
|
169
|
+
return { ok: false, issues };
|
|
170
|
+
}
|
|
123
171
|
if (account.status !== 200) {
|
|
124
172
|
issues.push(
|
|
125
173
|
`TFC token invalid (HTTP ${account.status}). Regenerate at https://app.terraform.io/app/settings/tokens.`
|
|
@@ -131,7 +179,11 @@ async function probeTfcToken(input) {
|
|
|
131
179
|
input.token,
|
|
132
180
|
`/organizations/${encodeURIComponent(input.organization)}`
|
|
133
181
|
);
|
|
134
|
-
if (org.status ===
|
|
182
|
+
if (org.status === 0) {
|
|
183
|
+
issues.push(
|
|
184
|
+
"Couldn't reach Terraform Cloud while checking the organization (network error or timeout)."
|
|
185
|
+
);
|
|
186
|
+
} else if (org.status === 404) {
|
|
135
187
|
issues.push(
|
|
136
188
|
`TFC organization "${input.organization}" not found, or this token lacks access. Check the org slug at https://app.terraform.io.`
|
|
137
189
|
);
|
|
@@ -582,16 +634,27 @@ async function waitForApply(input, budgets = DEFAULT_BUDGETS, deps = realDeps) {
|
|
|
582
634
|
}
|
|
583
635
|
return waitForRunOutput(input, budgets, deps, runId);
|
|
584
636
|
}
|
|
637
|
+
async function safePoll(fn, onRetry) {
|
|
638
|
+
try {
|
|
639
|
+
return await fn();
|
|
640
|
+
} catch {
|
|
641
|
+
onRetry?.();
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
585
645
|
async function discoverRunId(input, budgets, deps) {
|
|
586
646
|
deps.onProgress?.("discovery");
|
|
587
647
|
const discoveryStart = deps.now();
|
|
588
648
|
while (deps.now() - discoveryStart < budgets.discoveryMs) {
|
|
589
649
|
await deps.sleep(budgets.pollMs);
|
|
590
|
-
const ws = await
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
650
|
+
const ws = await safePoll(
|
|
651
|
+
() => deps.findWorkspace({
|
|
652
|
+
token: input.token,
|
|
653
|
+
organization: input.organization,
|
|
654
|
+
tags: input.workspaceTags
|
|
655
|
+
}),
|
|
656
|
+
() => deps.onProgress?.("retry")
|
|
657
|
+
);
|
|
595
658
|
if (ws?.currentRunId) return ws.currentRunId;
|
|
596
659
|
}
|
|
597
660
|
return null;
|
|
@@ -600,17 +663,20 @@ async function waitForRunOutput(input, budgets, deps, runId) {
|
|
|
600
663
|
deps.onProgress?.("run");
|
|
601
664
|
const runStart = deps.now();
|
|
602
665
|
while (deps.now() - runStart < budgets.runMs) {
|
|
603
|
-
const r = await
|
|
604
|
-
{ token: input.token, organization: input.organization },
|
|
605
|
-
|
|
666
|
+
const r = await safePoll(
|
|
667
|
+
() => deps.getRunStatus({ token: input.token, organization: input.organization }, runId),
|
|
668
|
+
() => deps.onProgress?.("retry")
|
|
606
669
|
);
|
|
607
670
|
if (r?.finished) {
|
|
608
671
|
if (!r.succeeded) return { kind: "run-failed", status: r.status };
|
|
609
672
|
deps.onProgress?.("fetching");
|
|
610
|
-
const value = await
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
673
|
+
const value = await safePoll(
|
|
674
|
+
() => deps.fetchOutput(
|
|
675
|
+
{ token: input.token, organization: input.organization },
|
|
676
|
+
input.workspaceId,
|
|
677
|
+
input.outputName
|
|
678
|
+
),
|
|
679
|
+
() => deps.onProgress?.("retry")
|
|
614
680
|
);
|
|
615
681
|
return value !== null ? { kind: "success", value } : { kind: "output-missing" };
|
|
616
682
|
}
|
|
@@ -1165,14 +1231,25 @@ function isoStampUtc(d) {
|
|
|
1165
1231
|
async function waitForApplyAndFetchTunnelToken(input) {
|
|
1166
1232
|
const spin = p3.spinner();
|
|
1167
1233
|
spin.start("Waiting for terraform apply to finish (polling every 10s)\u2026");
|
|
1168
|
-
const outcome = await waitForApply(
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1234
|
+
const outcome = await waitForApply(
|
|
1235
|
+
{
|
|
1236
|
+
token: input.token,
|
|
1237
|
+
organization: input.organization,
|
|
1238
|
+
workspaceId: input.workspaceId,
|
|
1239
|
+
runId: input.runId,
|
|
1240
|
+
workspaceTags: ["opuspopuli", "cloudflare"],
|
|
1241
|
+
outputName: "tunnel_token"
|
|
1242
|
+
},
|
|
1243
|
+
void 0,
|
|
1244
|
+
{
|
|
1245
|
+
...realDeps,
|
|
1246
|
+
// Surface a transient network hiccup instead of silently re-polling, so
|
|
1247
|
+
// a slow wait doesn't look like a hang.
|
|
1248
|
+
onProgress: (phase) => {
|
|
1249
|
+
if (phase === "retry") spin.message("Transient network error \u2014 retrying next poll\u2026");
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
);
|
|
1176
1253
|
switch (outcome.kind) {
|
|
1177
1254
|
case "success":
|
|
1178
1255
|
spin.stop(pc2.green("\u2713 Tunnel token retrieved."));
|
|
@@ -1192,16 +1269,6 @@ async function waitForApplyAndFetchTunnelToken(input) {
|
|
|
1192
1269
|
}
|
|
1193
1270
|
}
|
|
1194
1271
|
|
|
1195
|
-
// src/lib/constants.ts
|
|
1196
|
-
var PGSODIUM_KEY_RE = /^[a-f0-9]{64}$/;
|
|
1197
|
-
var TUNNEL_TOKEN_RE = /^[A-Za-z0-9_\-.=]+$/;
|
|
1198
|
-
var SAFE_PATH_RE = /^[A-Za-z0-9_\-./ ]+$/;
|
|
1199
|
-
var SAFE_LAUNCHCTL_VALUE_RE = /^[A-Za-z0-9+/=._-]+$/;
|
|
1200
|
-
var URL_SAFE_PASSWORD_RE = /^[A-Za-z0-9_-]+$/;
|
|
1201
|
-
var SAFE_URL_RE = /^[A-Za-z0-9:/_.-]+$/;
|
|
1202
|
-
var VERIFY_NETWORK_TIMEOUT_MS = 1e4;
|
|
1203
|
-
var BODY_PREVIEW_MAX = 200;
|
|
1204
|
-
|
|
1205
1272
|
// src/lib/homebrew.ts
|
|
1206
1273
|
var STUDIO_PACKAGES = [
|
|
1207
1274
|
{ name: "git", kind: "formula" },
|
|
@@ -4784,7 +4851,7 @@ async function looksLikeRegionsRepo(dir) {
|
|
|
4784
4851
|
}
|
|
4785
4852
|
|
|
4786
4853
|
// src/cli.ts
|
|
4787
|
-
var VERSION = "0.10.
|
|
4854
|
+
var VERSION = "0.10.13";
|
|
4788
4855
|
var program = new Command();
|
|
4789
4856
|
program.name("create-op-node").description(
|
|
4790
4857
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|