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 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
- - `qwen2.5:72b` — 72B, ~50 GB Ollama RAM. **Default**, recommended for
37
- 128 GB Studios. Best Spanish + multilingual quality.
38
- - `qwen2.5:32b` 32B, ~22 GB Ollama RAM. Middle tier for 64 GB Studios.
39
- - `qwen3.5:9b` — 9B, ~8 GB Ollama RAM. Validation runs or smaller
40
- Studios (36–48 GB).
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 res = await fetch(`${TFC_API}${path}`, { headers: authHeaders(token) });
107
- let body = null;
117
+ const ctrl = new AbortController();
118
+ const timer = setTimeout(() => ctrl.abort(), API_REQUEST_TIMEOUT_MS);
108
119
  try {
109
- body = await res.json();
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 === 404) {
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 deps.findWorkspace({
591
- token: input.token,
592
- organization: input.organization,
593
- tags: input.workspaceTags
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 deps.getRunStatus(
604
- { token: input.token, organization: input.organization },
605
- runId
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 deps.fetchOutput(
611
- { token: input.token, organization: input.organization },
612
- input.workspaceId,
613
- input.outputName
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
- token: input.token,
1170
- organization: input.organization,
1171
- workspaceId: input.workspaceId,
1172
- runId: input.runId,
1173
- workspaceTags: ["opuspopuli", "cloudflare"],
1174
- outputName: "tunnel_token"
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. Recommended for 128 GB Studios. Best Spanish + multilingual quality. Pull ~40 GB, 30\u201360 min."
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. Middle-tier for 64 GB Studios. Solid Spanish, faster than 72B. Pull ~20 GB, 15\u201330 min."
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. Validation / smaller Studios (36\u201348 GB). Pull ~5 GB, 3\u20135 min."
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.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."