@orkestrel/scaffold 0.0.64 → 0.0.66

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.
@@ -1,20 +1,329 @@
1
- #!/bin/bash
2
- # SessionStart loopback capability probe. Lifecycle stays user-owned and no
3
- # caller-controlled endpoint is contacted or copied into model context.
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
4
3
 
5
- if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
6
- exit 0
7
- fi
4
+ MAX_SECONDS=590
5
+ CLEANUP_SECONDS=5
6
+ OPERATION_SECONDS=$((MAX_SECONDS - CLEANUP_SECONDS - 1))
7
+ STARTED_SECONDS=$SECONDS
8
+ STARTUP_SECONDS=60
9
+ INSTALLER_URL='https://ollama.com/install.sh'
10
+
11
+ launched_pid=
12
+ installer_path=
13
+
14
+ fail() {
15
+ printf 'ollama.sh: %s\n' "$1" >&2
16
+ exit 1
17
+ }
18
+
19
+ remaining_seconds() {
20
+ local remaining=$((OPERATION_SECONDS - (SECONDS - STARTED_SECONDS)))
21
+ if ((remaining < 1)); then
22
+ printf '0\n'
23
+ else
24
+ printf '%s\n' "$remaining"
25
+ fi
26
+ }
27
+
28
+ terminate_group() {
29
+ local pid=$1
30
+ local deadline=$((SECONDS + CLEANUP_SECONDS))
31
+ kill -TERM -- "-$pid" >/dev/null 2>&1 || true
32
+ while kill -0 -- "-$pid" >/dev/null 2>&1; do
33
+ if ((SECONDS >= deadline)); then
34
+ kill -KILL -- "-$pid" >/dev/null 2>&1 || true
35
+ break
36
+ fi
37
+ sleep 1
38
+ done
39
+ wait "$pid" >/dev/null 2>&1 || true
40
+ }
41
+
42
+ cleanup() {
43
+ local status=$1
44
+ if [[ -n "$installer_path" && -f "$installer_path" ]]; then
45
+ rm -f -- "$installer_path"
46
+ fi
47
+ if ((status != 0)) && [[ -n "$launched_pid" ]]; then
48
+ terminate_group "$launched_pid"
49
+ fi
50
+ exit "$status"
51
+ }
52
+
53
+ normalize_host() {
54
+ OLLAMA_INPUT="$1" "$node_path" -e '
55
+ const { isIP } = require("node:net")
56
+ const raw = process.env.OLLAMA_INPUT
57
+ if (raw === undefined || raw.length === 0) process.exit(1)
58
+ let url
59
+ try {
60
+ url = new URL(/^https?:\/\//iu.test(raw) ? raw : `http://${raw}`)
61
+ } catch {
62
+ process.exit(1)
63
+ }
64
+ if (
65
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
66
+ url.username !== "" ||
67
+ url.password !== "" ||
68
+ url.pathname !== "/" ||
69
+ url.search !== "" ||
70
+ url.hash !== "" ||
71
+ url.hostname === ""
72
+ ) process.exit(1)
73
+ const hostname =
74
+ url.hostname.startsWith("[") && url.hostname.endsWith("]")
75
+ ? url.hostname.slice(1, -1)
76
+ : url.hostname
77
+ const family = isIP(hostname)
78
+ const loopback =
79
+ hostname === "localhost" ||
80
+ (family === 4 && hostname.split(".")[0] === "127") ||
81
+ (family === 6 && hostname === "::1")
82
+ process.stdout.write(`${url.origin}\n${loopback ? "true" : "false"}`)
83
+ '
84
+ }
85
+
86
+ build_payload() {
87
+ OLLAMA_MODEL_VALUE="$model" "$node_path" -e '
88
+ const model = process.env.OLLAMA_MODEL_VALUE
89
+ if (model === undefined || model.length === 0) process.exit(1)
90
+ let body
91
+ switch (process.argv[1]) {
92
+ case "show":
93
+ body = { model }
94
+ break
95
+ case "pull":
96
+ body = { model, stream: false }
97
+ break
98
+ case "chat":
99
+ body = {
100
+ model,
101
+ messages: [{ role: "user", content: "hi" }],
102
+ stream: false,
103
+ think: false,
104
+ keep_alive: "30m",
105
+ options: { num_predict: 1 },
106
+ }
107
+ break
108
+ default:
109
+ process.exit(1)
110
+ }
111
+ process.stdout.write(JSON.stringify(body))
112
+ ' "$1"
113
+ }
114
+
115
+ validate_response() {
116
+ "$node_path" -e '
117
+ const { readFileSync } = require("node:fs")
118
+ let body
119
+ try {
120
+ body = JSON.parse(readFileSync(0, "utf8"))
121
+ } catch {
122
+ process.exit(1)
123
+ }
124
+ if (body === null || typeof body !== "object" || Array.isArray(body)) process.exit(1)
125
+ if (process.argv[1] === "pull") process.exit(body.status === "success" ? 0 : 1)
126
+ if (process.argv[1] === "chat") process.exit(body.done === true ? 0 : 1)
127
+ process.exit(1)
128
+ ' "$1"
129
+ }
8
130
 
9
- if ! command -v ollama >/dev/null 2>&1; then
10
- echo "ollama.sh: loopback capability unavailable."
11
- exit 0
131
+ probe_version() {
132
+ local allowance=$1
133
+ local status
134
+ if ! status=$(curl --silent --max-time "$allowance" --output /dev/null \
135
+ --write-out '%{http_code}' "$normalized_host/api/version" 2>/dev/null); then
136
+ return 1
137
+ fi
138
+ [[ "$status" =~ ^2[0-9][0-9]$ ]]
139
+ }
140
+
141
+ trap 'cleanup $?' EXIT
142
+
143
+ if ! command -v node >/dev/null 2>&1; then
144
+ printf 'ollama.sh: node is required for URL and JSON handling\n' >&2
145
+ exit 127
12
146
  fi
147
+ if ! command -v curl >/dev/null 2>&1; then
148
+ printf 'ollama.sh: curl is required for Ollama setup\n' >&2
149
+ exit 127
150
+ fi
151
+
152
+ node_path=$(command -v node)
153
+ script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
154
+ project_root=$(cd -- "$script_dir/.." && pwd -P)
155
+ tmp_dir="$project_root/tmp"
156
+ mkdir -p -- "$tmp_dir"
13
157
 
14
- if curl -sf --max-time 2 "http://127.0.0.1:11434/api/tags" >/dev/null 2>&1; then
15
- echo "ollama.sh: loopback capability ready; lifecycle remains user-owned."
158
+ if [[ ${OLLAMA_HOST+x} == x ]]; then
159
+ host_input=$OLLAMA_HOST
16
160
  else
17
- echo "ollama.sh: loopback capability unreachable; lifecycle remains user-owned."
161
+ host_input='http://127.0.0.1:11434'
162
+ fi
163
+ if [[ ${OLLAMA_MODEL+x} == x ]]; then
164
+ model=$OLLAMA_MODEL
165
+ else
166
+ model='qwen3.5:2b-q4_K_M'
167
+ fi
168
+ if [[ -z "$model" ]]; then
169
+ fail 'OLLAMA_MODEL must not be empty'
170
+ fi
171
+
172
+ if ! host_data=$(normalize_host "$host_input"); then
173
+ fail 'OLLAMA_HOST must be an HTTP origin without credentials, path, query, or fragment'
174
+ fi
175
+ if [[ "$host_data" != *$'\n'* ]]; then
176
+ fail 'OLLAMA_HOST normalization returned no endpoint classification'
177
+ fi
178
+ normalized_host=${host_data%%$'\n'*}
179
+ loopback=${host_data#*$'\n'}
180
+
181
+ ready=false
182
+ if probe_version 2; then
183
+ ready=true
184
+ fi
185
+
186
+ if [[ "$ready" != true ]]; then
187
+ if [[ "$loopback" != true || "$normalized_host" != http://* ]]; then
188
+ fail 'the configured endpoint is unreachable; local startup is limited to HTTP loopback'
189
+ fi
190
+
191
+ if ! command -v ollama >/dev/null 2>&1; then
192
+ if [[ ${CLAUDE_CODE_REMOTE:-} != true && ${CI:-} != true ]]; then
193
+ printf 'ollama.sh: ollama is required to start an unreachable loopback endpoint\n' >&2
194
+ exit 127
195
+ fi
196
+ if [[ $(uname -s) != Linux ]]; then
197
+ fail 'automatic Ollama installation is supported only on Linux'
198
+ fi
199
+ if ! command -v timeout >/dev/null 2>&1; then
200
+ printf 'ollama.sh: timeout is required to bound automatic installation\n' >&2
201
+ exit 127
202
+ fi
203
+ if ! command -v mktemp >/dev/null 2>&1; then
204
+ printf 'ollama.sh: mktemp is required for automatic installation\n' >&2
205
+ exit 127
206
+ fi
207
+ installer_path=$(mktemp "$tmp_dir/ollama-install.XXXXXX.sh")
208
+ remaining=$(remaining_seconds)
209
+ if ((remaining == 0)); then
210
+ fail 'the setup deadline expired before installation'
211
+ fi
212
+ if ! curl --fail --silent --show-error --location \
213
+ --proto '=https' --proto-redir '=https' --max-time "$remaining" \
214
+ --output "$installer_path" "$INSTALLER_URL"; then
215
+ fail 'the official Ollama installer download failed'
216
+ fi
217
+ if [[ ! -s "$installer_path" ]]; then
218
+ fail 'the official Ollama installer download was empty'
219
+ fi
220
+ remaining=$(remaining_seconds)
221
+ if ((remaining == 0)); then
222
+ fail 'the setup deadline expired before installation'
223
+ fi
224
+ if ! timeout --signal=TERM --kill-after=5s "${remaining}s" sh "$installer_path"; then
225
+ fail 'the official Ollama installer did not complete within the setup deadline'
226
+ fi
227
+ ollama_path=$(command -v ollama || true)
228
+ if [[ -z "$ollama_path" || ! -f "$ollama_path" || ! -x "$ollama_path" ]]; then
229
+ fail 'the official Ollama installer completed without an executable in PATH'
230
+ fi
231
+ remaining=$(remaining_seconds)
232
+ if ((remaining == 0)); then
233
+ fail 'the setup deadline expired after installation'
234
+ fi
235
+ probe_allowance=2
236
+ if ((remaining < probe_allowance)); then probe_allowance=$remaining; fi
237
+ if probe_version "$probe_allowance"; then
238
+ ready=true
239
+ fi
240
+ fi
241
+
242
+ if [[ "$ready" != true ]]; then
243
+ host_kernel=$(uname -s)
244
+ if [[ "$host_kernel" == MINGW* || "$host_kernel" == MSYS* || "$host_kernel" == CYGWIN* ]]; then
245
+ fail 'starting Ollama from Git Bash is unsupported because failure cleanup cannot terminate its Windows process tree safely'
246
+ fi
247
+ if ! command -v setsid >/dev/null 2>&1; then
248
+ fail 'setsid is required to own an Ollama daemon process group for failure cleanup'
249
+ fi
250
+ ollama_path=$(command -v ollama)
251
+ log_path="$tmp_dir/ollama-service.log"
252
+ OLLAMA_HOST="$normalized_host" setsid "$ollama_path" serve </dev/null >>"$log_path" 2>&1 &
253
+ launched_pid=$!
254
+ startup_started=$SECONDS
255
+ while true; do
256
+ remaining=$(remaining_seconds)
257
+ startup_remaining=$((STARTUP_SECONDS - (SECONDS - startup_started)))
258
+ if ((remaining == 0 || startup_remaining <= 0)); then
259
+ fail 'Ollama did not become ready within the startup deadline; see tmp/ollama-service.log'
260
+ fi
261
+ probe_allowance=2
262
+ if ((remaining < probe_allowance)); then probe_allowance=$remaining; fi
263
+ if ((startup_remaining < probe_allowance)); then probe_allowance=$startup_remaining; fi
264
+ if probe_version "$probe_allowance"; then
265
+ ready=true
266
+ break
267
+ fi
268
+ sleep 1
269
+ done
270
+ fi
271
+ fi
272
+
273
+ show_payload=$(build_payload show) || fail 'failed to serialize the model request'
274
+ remaining=$(remaining_seconds)
275
+ if ((remaining == 0)); then fail 'the setup deadline expired before model inspection'; fi
276
+ if ! show_status=$(curl --silent --show-error --max-time "$remaining" \
277
+ --output /dev/null --write-out '%{http_code}' \
278
+ --header 'Content-Type: application/json' --data-binary "$show_payload" \
279
+ "$normalized_host/api/show"); then
280
+ fail 'the model inspection request failed'
281
+ fi
282
+
283
+ if [[ "$show_status" == 404 ]]; then
284
+ pull_payload=$(build_payload pull) || fail 'failed to serialize the pull request'
285
+ remaining=$(remaining_seconds)
286
+ if ((remaining == 0)); then fail 'the setup deadline expired before model pull'; fi
287
+ if ! pull_wire=$(curl --silent --show-error --max-time "$remaining" \
288
+ --write-out $'\n%{http_code}' \
289
+ --header 'Content-Type: application/json' --data-binary "$pull_payload" \
290
+ "$normalized_host/api/pull"); then
291
+ fail 'the model pull request failed'
292
+ fi
293
+ if [[ "$pull_wire" != *$'\n'* ]]; then
294
+ fail 'the model pull response carried no HTTP status'
295
+ fi
296
+ pull_status=${pull_wire##*$'\n'}
297
+ pull_response=${pull_wire%$'\n'*}
298
+ if [[ ! "$pull_status" =~ ^2[0-9][0-9]$ ]]; then
299
+ fail 'the model pull request returned an unsuccessful HTTP status'
300
+ fi
301
+ if ! printf '%s' "$pull_response" | validate_response pull; then
302
+ fail 'the model pull response did not report completion'
303
+ fi
304
+ elif [[ ! "$show_status" =~ ^2[0-9][0-9]$ ]]; then
305
+ fail 'the model inspection request failed without reporting absence'
306
+ fi
307
+
308
+ chat_payload=$(build_payload chat) || fail 'failed to serialize the warm request'
309
+ remaining=$(remaining_seconds)
310
+ if ((remaining == 0)); then fail 'the setup deadline expired before model warmup'; fi
311
+ if ! chat_wire=$(curl --silent --show-error --max-time "$remaining" \
312
+ --write-out $'\n%{http_code}' \
313
+ --header 'Content-Type: application/json' --data-binary "$chat_payload" \
314
+ "$normalized_host/api/chat"); then
315
+ fail 'the model warm request failed'
316
+ fi
317
+ if [[ "$chat_wire" != *$'\n'* ]]; then
318
+ fail 'the model warm response carried no HTTP status'
319
+ fi
320
+ chat_status=${chat_wire##*$'\n'}
321
+ chat_response=${chat_wire%$'\n'*}
322
+ if [[ ! "$chat_status" =~ ^2[0-9][0-9]$ ]]; then
323
+ fail 'the model warm request returned an unsuccessful HTTP status'
324
+ fi
325
+ if ! printf '%s' "$chat_response" | validate_response chat; then
326
+ fail 'the model warm response did not report completion'
18
327
  fi
19
328
 
20
- exit 0
329
+ printf 'ollama.sh: Ollama is ready\n'
@@ -4,7 +4,7 @@ let _orkestrel_template = require("@orkestrel/template");
4
4
  let _orkestrel_emitter = require("@orkestrel/emitter");
5
5
  var package_default = {
6
6
  name: "@orkestrel/scaffold",
7
- version: "0.0.64",
7
+ version: "0.0.66",
8
8
  description: "Scaffold workspaces with commands: new, audit, repair, catalog, and overwrite.",
9
9
  keywords: [
10
10
  "audit",
@@ -94,14 +94,14 @@ var package_default = {
94
94
  "@orkestrel/contract": "^0.0.17",
95
95
  "@orkestrel/emitter": "^0.0.10",
96
96
  "@orkestrel/markdown": "^0.0.14",
97
- "@orkestrel/process": "^0.0.11",
97
+ "@orkestrel/process": "^0.0.12",
98
98
  "@orkestrel/template": "^0.0.7"
99
99
  },
100
100
  devDependencies: {
101
101
  "@microsoft/api-extractor": "^7.59.0",
102
- "@orkestrel/guide": "^0.0.17",
102
+ "@orkestrel/guide": "^0.0.18",
103
103
  "@orkestrel/html": "^0.0.9",
104
- "@orkestrel/probe": "^0.0.12",
104
+ "@orkestrel/probe": "^0.0.13",
105
105
  "@orkestrel/test": "^0.0.14",
106
106
  "@types/node": "^26.5.0",
107
107
  "@vitest/browser-playwright": "^4.1.11",
@@ -112,7 +112,7 @@ var package_default = {
112
112
  "vite": "^8.2.2",
113
113
  "vitest": "^4.1.11"
114
114
  },
115
- engines: { "node": ">=22.12.0" }
115
+ engines: { "node": ">=22.18.0" }
116
116
  };
117
117
  //#endregion
118
118
  //#region src/core/constants.ts
@@ -384,7 +384,7 @@ var CATALOG_OPENING_MARKER = "<!-- orkestrel:catalog -->";
384
384
  * refused rather than rewritten.
385
385
  */
386
386
  var CATALOG_CLOSING_MARKER = "<!-- /orkestrel:catalog -->";
387
- /** Names the provisioner skeleton a workspace with declared service vendors is given once. */
387
+ /** Names the inventory skeleton a workspace with declared service vendors is given once. */
388
388
  var SERVICE_SCRIPT_PATH = "scripts/service.sh";
389
389
  /** Names the shared Vitest global-setup module whose presence makes a workspace `global`. */
390
390
  var GLOBAL_SETUP_PATH = "tests/setupGlobal.ts";
@@ -527,11 +527,29 @@ var MAX_MANIFEST_BYTES = 1048576;
527
527
  /** Caps the bytes retained across one whole plan or audit. */
528
528
  var MAX_TOTAL_ARTIFACT_BYTES = 104857600;
529
529
  /** Names the oldest Node version the generated toolchain supports. */
530
- var MINIMUM_NODE_VERSION = "22.12.0";
530
+ var MINIMUM_NODE_VERSION = "22.18.0";
531
+ /** Names the oldest npm version the generated toolchain supports. */
532
+ var MINIMUM_NPM_VERSION = "11.6.0";
531
533
  /** Names the version a workspace starts at. */
532
534
  var DEFAULT_VERSION = "0.0.1";
533
535
  /** Names the `engines.node` range a workspace starts with. */
534
536
  var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
537
+ /**
538
+ * Holds the `devEngines` record every generated manifest carries.
539
+ *
540
+ * @remarks
541
+ * Every generated manifest names npm at the {@link MINIMUM_NPM_VERSION} floor with the `onFail`
542
+ * key set to the `error` value, and no blueprint field varies that record. An npm at 10.9.0 or
543
+ * later reads the `devEngines` record. Such an npm earlier than the floor refuses an install in a
544
+ * generated workspace rather than resolving its dependency graph.
545
+ * The neighbouring `DEFAULT_ENGINES` constant is the Node range, and a blueprint's `engines`
546
+ * field does replace that one.
547
+ */
548
+ var WORKSPACE_DEV_ENGINES = Object.freeze({ packageManager: Object.freeze({
549
+ name: "npm",
550
+ version: `>=${MINIMUM_NPM_VERSION}`,
551
+ onFail: "error"
552
+ }) });
535
553
  /** Holds the tooling versions scaffold and every generated workspace share. */
536
554
  var BASE_DEV_DEPENDENCIES = Object.freeze({
537
555
  "@orkestrel/guide": package_default.devDependencies["@orkestrel/guide"],
@@ -936,9 +954,9 @@ export const conformance = (): UserConfig => ({
936
954
  },
937
955
  })
938
956
  `,
939
- service: `// The live external services this package drives. It starts nothing itself:
940
- // \`scripts/service.sh\` provisions, \`tests/setupService.ts\` proves readiness, and
941
- // the project stays out of \`npm test\` because a real service answers it.
957
+ service: `// The caller prepares the live external services before this project.
958
+ // \`tests/setupService.ts\` verifies readiness, and the project stays out of \`npm test\`
959
+ // because a real service answers it.
942
960
  export const service = (): UserConfig => ({
943
961
  resolve,
944
962
  test: {
@@ -4287,9 +4305,9 @@ function matchesRange(range, latest) {
4287
4305
  * ```ts
4288
4306
  * import { matchesEngines } from '@orkestrel/scaffold'
4289
4307
  *
4290
- * matchesEngines('>=22.12.0') // true
4308
+ * matchesEngines('>=22.18.0') // true
4291
4309
  * matchesEngines('>=20.0.0') // false
4292
- * matchesEngines('22.12.0') // false
4310
+ * matchesEngines('22.18.0') // false
4293
4311
  * ```
4294
4312
  */
4295
4313
  function matchesEngines(engines) {
@@ -4838,6 +4856,7 @@ function blueprintToManifest(blueprint) {
4838
4856
  devDependencies: blueprintToDevDependencies(blueprint),
4839
4857
  ...Object.keys(peerDependencies).length > 0 ? { peerDependencies } : {},
4840
4858
  ...Object.keys(peerDependenciesMeta).length > 0 ? { peerDependenciesMeta } : {},
4859
+ devEngines: WORKSPACE_DEV_ENGINES,
4841
4860
  engines: { node: blueprint.engines }
4842
4861
  };
4843
4862
  return `${JSON.stringify(manifest, void 0, " ")}\n`;
@@ -6581,7 +6600,7 @@ function overridesToQuestions(overrides, artifacts) {
6581
6600
  * })
6582
6601
  *
6583
6602
  * blueprint.version // '0.0.1'
6584
- * blueprint.engines // '>=22.12.0'
6603
+ * blueprint.engines // '>=22.18.0'
6585
6604
  * ```
6586
6605
  */
6587
6606
  function createBlueprint(name, input) {
@@ -6964,6 +6983,7 @@ exports.MAX_SCRIPT_LENGTH = MAX_SCRIPT_LENGTH;
6964
6983
  exports.MAX_TOTAL_ARTIFACT_BYTES = MAX_TOTAL_ARTIFACT_BYTES;
6965
6984
  exports.MAX_TOTAL_REGISTRY_BYTES = MAX_TOTAL_REGISTRY_BYTES;
6966
6985
  exports.MINIMUM_NODE_VERSION = MINIMUM_NODE_VERSION;
6986
+ exports.MINIMUM_NPM_VERSION = MINIMUM_NPM_VERSION;
6967
6987
  exports.NAME_PATTERN = NAME_PATTERN;
6968
6988
  exports.ORCHESTRATION_PATH_NAMES = ORCHESTRATION_PATH_NAMES;
6969
6989
  exports.ORCHESTRATION_PATH_PREFIXES = ORCHESTRATION_PATH_PREFIXES;
@@ -6980,6 +7000,7 @@ exports.SRC_MATRIX = SRC_MATRIX;
6980
7000
  exports.ScaffoldError = ScaffoldError;
6981
7001
  exports.TAB_WIDTH = TAB_WIDTH;
6982
7002
  exports.VERSION_PATTERN = VERSION_PATTERN;
7003
+ exports.WORKSPACE_DEV_ENGINES = WORKSPACE_DEV_ENGINES;
6983
7004
  exports.WORKSPACE_OWNED_PATHS = WORKSPACE_OWNED_PATHS;
6984
7005
  exports.applyOverrides = applyOverrides;
6985
7006
  exports.artifactToFinding = artifactToFinding;