create-op-node 0.10.10 → 0.10.12
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/README.md +12 -7
- package/dist/cli.js +86 -39
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,15 +31,20 @@ Configures macOS power settings, installs Homebrew + the CLI tool list, sets up
|
|
|
31
31
|
### Choosing the LLM model
|
|
32
32
|
|
|
33
33
|
When you run `bootstrap` interactively (no `-y`, no `--llm-model` flag),
|
|
34
|
-
you'll get a curated picker
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- `
|
|
40
|
-
|
|
34
|
+
you'll get a curated picker that **pre-selects** a model based on the
|
|
35
|
+
Studio's detected unified memory:
|
|
36
|
+
|
|
37
|
+
- `qwen2.5:72b` — 72B, ~50 GB Ollama RAM. Pre-selected on 96 GB+ Studios.
|
|
38
|
+
Best Spanish + multilingual quality.
|
|
39
|
+
- `qwen2.5:32b` — 32B, ~22 GB Ollama RAM. Pre-selected on 48–95 GB Studios
|
|
40
|
+
(e.g. 64 GB).
|
|
41
|
+
- `qwen3.5:9b` — 9B, ~8 GB Ollama RAM. Pre-selected on smaller Studios
|
|
42
|
+
(< 48 GB), and the default for non-interactive `-y` runs (see below).
|
|
41
43
|
- `Other…` — any Ollama model name you specify.
|
|
42
44
|
|
|
45
|
+
(If memory detection fails, the picker falls back to pre-selecting
|
|
46
|
+
`qwen2.5:72b`.)
|
|
47
|
+
|
|
43
48
|
The picker is Qwen-only because the Opus Populi platform serves
|
|
44
49
|
Spanish-speaking civic users and Qwen has the strongest Spanish (and
|
|
45
50
|
broader multilingual) capability of the open-weight models in this
|
package/dist/cli.js
CHANGED
|
@@ -90,6 +90,17 @@ async function tunnelStatus(input) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// src/lib/constants.ts
|
|
94
|
+
var PGSODIUM_KEY_RE = /^[a-f0-9]{64}$/;
|
|
95
|
+
var TUNNEL_TOKEN_RE = /^[A-Za-z0-9_\-.=]+$/;
|
|
96
|
+
var SAFE_PATH_RE = /^[A-Za-z0-9_\-./ ]+$/;
|
|
97
|
+
var SAFE_LAUNCHCTL_VALUE_RE = /^[A-Za-z0-9+/=._-]+$/;
|
|
98
|
+
var URL_SAFE_PASSWORD_RE = /^[A-Za-z0-9_-]+$/;
|
|
99
|
+
var SAFE_URL_RE = /^[A-Za-z0-9:/_.-]+$/;
|
|
100
|
+
var VERIFY_NETWORK_TIMEOUT_MS = 1e4;
|
|
101
|
+
var API_REQUEST_TIMEOUT_MS = 15e3;
|
|
102
|
+
var BODY_PREVIEW_MAX = 200;
|
|
103
|
+
|
|
93
104
|
// src/lib/tfc.ts
|
|
94
105
|
var TFC_API = "https://app.terraform.io/api/v2";
|
|
95
106
|
var ORG_SLUG_RE = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -103,13 +114,24 @@ function authHeaders(token) {
|
|
|
103
114
|
};
|
|
104
115
|
}
|
|
105
116
|
async function getJson(token, path) {
|
|
106
|
-
const
|
|
107
|
-
|
|
117
|
+
const ctrl = new AbortController();
|
|
118
|
+
const timer = setTimeout(() => ctrl.abort(), API_REQUEST_TIMEOUT_MS);
|
|
108
119
|
try {
|
|
109
|
-
|
|
120
|
+
const res = await fetch(`${TFC_API}${path}`, {
|
|
121
|
+
headers: authHeaders(token),
|
|
122
|
+
signal: ctrl.signal
|
|
123
|
+
});
|
|
124
|
+
let body = null;
|
|
125
|
+
try {
|
|
126
|
+
body = await res.json();
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
return { status: res.status, body };
|
|
110
130
|
} catch {
|
|
131
|
+
return { status: 0, body: null };
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timer);
|
|
111
134
|
}
|
|
112
|
-
return { status: res.status, body };
|
|
113
135
|
}
|
|
114
136
|
async function probeTfcToken(input) {
|
|
115
137
|
const issues = [];
|
|
@@ -120,6 +142,12 @@ async function probeTfcToken(input) {
|
|
|
120
142
|
return { ok: false, issues };
|
|
121
143
|
}
|
|
122
144
|
const account = await getJson(input.token, "/account/details");
|
|
145
|
+
if (account.status === 0) {
|
|
146
|
+
issues.push(
|
|
147
|
+
"Couldn't reach Terraform Cloud (network error or timeout). Check your connection and retry."
|
|
148
|
+
);
|
|
149
|
+
return { ok: false, issues };
|
|
150
|
+
}
|
|
123
151
|
if (account.status !== 200) {
|
|
124
152
|
issues.push(
|
|
125
153
|
`TFC token invalid (HTTP ${account.status}). Regenerate at https://app.terraform.io/app/settings/tokens.`
|
|
@@ -131,7 +159,11 @@ async function probeTfcToken(input) {
|
|
|
131
159
|
input.token,
|
|
132
160
|
`/organizations/${encodeURIComponent(input.organization)}`
|
|
133
161
|
);
|
|
134
|
-
if (org.status ===
|
|
162
|
+
if (org.status === 0) {
|
|
163
|
+
issues.push(
|
|
164
|
+
"Couldn't reach Terraform Cloud while checking the organization (network error or timeout)."
|
|
165
|
+
);
|
|
166
|
+
} else if (org.status === 404) {
|
|
135
167
|
issues.push(
|
|
136
168
|
`TFC organization "${input.organization}" not found, or this token lacks access. Check the org slug at https://app.terraform.io.`
|
|
137
169
|
);
|
|
@@ -582,16 +614,27 @@ async function waitForApply(input, budgets = DEFAULT_BUDGETS, deps = realDeps) {
|
|
|
582
614
|
}
|
|
583
615
|
return waitForRunOutput(input, budgets, deps, runId);
|
|
584
616
|
}
|
|
617
|
+
async function safePoll(fn, onRetry) {
|
|
618
|
+
try {
|
|
619
|
+
return await fn();
|
|
620
|
+
} catch {
|
|
621
|
+
onRetry?.();
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
585
625
|
async function discoverRunId(input, budgets, deps) {
|
|
586
626
|
deps.onProgress?.("discovery");
|
|
587
627
|
const discoveryStart = deps.now();
|
|
588
628
|
while (deps.now() - discoveryStart < budgets.discoveryMs) {
|
|
589
629
|
await deps.sleep(budgets.pollMs);
|
|
590
|
-
const ws = await
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
630
|
+
const ws = await safePoll(
|
|
631
|
+
() => deps.findWorkspace({
|
|
632
|
+
token: input.token,
|
|
633
|
+
organization: input.organization,
|
|
634
|
+
tags: input.workspaceTags
|
|
635
|
+
}),
|
|
636
|
+
() => deps.onProgress?.("retry")
|
|
637
|
+
);
|
|
595
638
|
if (ws?.currentRunId) return ws.currentRunId;
|
|
596
639
|
}
|
|
597
640
|
return null;
|
|
@@ -600,17 +643,20 @@ async function waitForRunOutput(input, budgets, deps, runId) {
|
|
|
600
643
|
deps.onProgress?.("run");
|
|
601
644
|
const runStart = deps.now();
|
|
602
645
|
while (deps.now() - runStart < budgets.runMs) {
|
|
603
|
-
const r = await
|
|
604
|
-
{ token: input.token, organization: input.organization },
|
|
605
|
-
|
|
646
|
+
const r = await safePoll(
|
|
647
|
+
() => deps.getRunStatus({ token: input.token, organization: input.organization }, runId),
|
|
648
|
+
() => deps.onProgress?.("retry")
|
|
606
649
|
);
|
|
607
650
|
if (r?.finished) {
|
|
608
651
|
if (!r.succeeded) return { kind: "run-failed", status: r.status };
|
|
609
652
|
deps.onProgress?.("fetching");
|
|
610
|
-
const value = await
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
653
|
+
const value = await safePoll(
|
|
654
|
+
() => deps.fetchOutput(
|
|
655
|
+
{ token: input.token, organization: input.organization },
|
|
656
|
+
input.workspaceId,
|
|
657
|
+
input.outputName
|
|
658
|
+
),
|
|
659
|
+
() => deps.onProgress?.("retry")
|
|
614
660
|
);
|
|
615
661
|
return value !== null ? { kind: "success", value } : { kind: "output-missing" };
|
|
616
662
|
}
|
|
@@ -1165,14 +1211,25 @@ function isoStampUtc(d) {
|
|
|
1165
1211
|
async function waitForApplyAndFetchTunnelToken(input) {
|
|
1166
1212
|
const spin = p3.spinner();
|
|
1167
1213
|
spin.start("Waiting for terraform apply to finish (polling every 10s)\u2026");
|
|
1168
|
-
const outcome = await waitForApply(
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1214
|
+
const outcome = await waitForApply(
|
|
1215
|
+
{
|
|
1216
|
+
token: input.token,
|
|
1217
|
+
organization: input.organization,
|
|
1218
|
+
workspaceId: input.workspaceId,
|
|
1219
|
+
runId: input.runId,
|
|
1220
|
+
workspaceTags: ["opuspopuli", "cloudflare"],
|
|
1221
|
+
outputName: "tunnel_token"
|
|
1222
|
+
},
|
|
1223
|
+
void 0,
|
|
1224
|
+
{
|
|
1225
|
+
...realDeps,
|
|
1226
|
+
// Surface a transient network hiccup instead of silently re-polling, so
|
|
1227
|
+
// a slow wait doesn't look like a hang.
|
|
1228
|
+
onProgress: (phase) => {
|
|
1229
|
+
if (phase === "retry") spin.message("Transient network error \u2014 retrying next poll\u2026");
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
);
|
|
1176
1233
|
switch (outcome.kind) {
|
|
1177
1234
|
case "success":
|
|
1178
1235
|
spin.stop(pc2.green("\u2713 Tunnel token retrieved."));
|
|
@@ -1192,16 +1249,6 @@ async function waitForApplyAndFetchTunnelToken(input) {
|
|
|
1192
1249
|
}
|
|
1193
1250
|
}
|
|
1194
1251
|
|
|
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
1252
|
// src/lib/homebrew.ts
|
|
1206
1253
|
var STUDIO_PACKAGES = [
|
|
1207
1254
|
{ name: "git", kind: "formula" },
|
|
@@ -2697,17 +2744,17 @@ var LLM_MODEL_CHOICES = [
|
|
|
2697
2744
|
{
|
|
2698
2745
|
value: "qwen2.5:72b",
|
|
2699
2746
|
label: "qwen2.5:72b",
|
|
2700
|
-
hint: "72B, ~50 GB Ollama RAM.
|
|
2747
|
+
hint: "72B, ~50 GB Ollama RAM. Pre-selected on 96 GB+ Studios. Best Spanish + multilingual quality. Pull ~40 GB, 30\u201360 min."
|
|
2701
2748
|
},
|
|
2702
2749
|
{
|
|
2703
2750
|
value: "qwen2.5:32b",
|
|
2704
2751
|
label: "qwen2.5:32b",
|
|
2705
|
-
hint: "32B, ~22 GB Ollama RAM.
|
|
2752
|
+
hint: "32B, ~22 GB Ollama RAM. Pre-selected on 48\u201395 GB Studios. Solid Spanish, faster than 72B. Pull ~20 GB, 15\u201330 min."
|
|
2706
2753
|
},
|
|
2707
2754
|
{
|
|
2708
2755
|
value: "qwen3.5:9b",
|
|
2709
2756
|
label: "qwen3.5:9b",
|
|
2710
|
-
hint: "9B, ~8 GB Ollama RAM.
|
|
2757
|
+
hint: "9B, ~8 GB Ollama RAM. Pre-selected on smaller Studios (< 48 GB); also the -y / scripted default. Pull ~5 GB, 3\u20135 min."
|
|
2711
2758
|
}
|
|
2712
2759
|
];
|
|
2713
2760
|
var OTHER_SENTINEL = "__OTHER__";
|
|
@@ -4784,7 +4831,7 @@ async function looksLikeRegionsRepo(dir) {
|
|
|
4784
4831
|
}
|
|
4785
4832
|
|
|
4786
4833
|
// src/cli.ts
|
|
4787
|
-
var VERSION = "0.10.
|
|
4834
|
+
var VERSION = "0.10.12";
|
|
4788
4835
|
var program = new Command();
|
|
4789
4836
|
program.name("create-op-node").description(
|
|
4790
4837
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|