@turri-dev/cli 0.2.0 → 0.2.1-next.bc56a0d

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.
Files changed (80) hide show
  1. package/README.md +3 -2
  2. package/dist/chats.d.ts +31 -4
  3. package/dist/chats.js +146 -52
  4. package/dist/cli.js +4 -2
  5. package/dist/config.d.ts +4 -1
  6. package/dist/config.js +39 -4
  7. package/dist/environment.d.ts +0 -2
  8. package/dist/environment.js +0 -6
  9. package/dist/extension.d.ts +4 -3
  10. package/dist/extension.js +109 -22
  11. package/dist/host-session.d.ts +14 -0
  12. package/dist/host-session.js +2 -0
  13. package/dist/host.d.ts +6 -1
  14. package/dist/host.js +5 -1
  15. package/dist/init-config.d.ts +1 -1
  16. package/dist/init-config.js +33 -3
  17. package/dist/installed-providers.d.ts +40 -0
  18. package/dist/installed-providers.js +98 -0
  19. package/dist/model-check.d.ts +4 -2
  20. package/dist/model-check.js +33 -17
  21. package/dist/notice.d.ts +30 -6
  22. package/dist/pi.js +8 -24
  23. package/dist/plain/files.d.ts +16 -0
  24. package/dist/plain/files.js +68 -5
  25. package/dist/plain/graph.js +2 -2
  26. package/dist/plain/module-concepts.js +3 -2
  27. package/dist/plain/parser.d.ts +8 -3
  28. package/dist/plain/parser.js +47 -6
  29. package/dist/plain/stdlib.js +3 -3
  30. package/dist/regenerate/base-folder.d.ts +2 -0
  31. package/dist/regenerate/base-folder.js +24 -6
  32. package/dist/regenerate/command-check.d.ts +12 -1
  33. package/dist/regenerate/command-check.js +74 -0
  34. package/dist/regenerate/confine.d.ts +5 -0
  35. package/dist/regenerate/confine.js +9 -9
  36. package/dist/regenerate/ledger.d.ts +7 -0
  37. package/dist/regenerate/lineage.js +43 -2
  38. package/dist/regenerate/loop.d.ts +8 -0
  39. package/dist/regenerate/loop.js +11 -4
  40. package/dist/regenerate/os-sandbox.d.ts +2 -0
  41. package/dist/regenerate/os-sandbox.js +55 -20
  42. package/dist/regenerate/release.d.ts +2 -0
  43. package/dist/regenerate/requires.d.ts +12 -0
  44. package/dist/regenerate/requires.js +70 -1
  45. package/dist/regenerate/spec-steps.js +18 -1
  46. package/dist/regenerate/standing.d.ts +28 -0
  47. package/dist/regenerate/standing.js +30 -0
  48. package/dist/regenerate/testing.d.ts +2 -0
  49. package/dist/regenerate/testing.js +14 -0
  50. package/dist/release-agent.d.ts +17 -0
  51. package/dist/release-agent.js +19 -7
  52. package/dist/release-tools.d.ts +6 -0
  53. package/dist/release-tools.js +24 -5
  54. package/dist/sentinel-chat.d.ts +1 -1
  55. package/dist/sentinel-chat.js +21 -8
  56. package/dist/sentinel-check.d.ts +36 -0
  57. package/dist/sentinel-check.js +78 -0
  58. package/dist/sentinel.d.ts +40 -9
  59. package/dist/sentinel.js +240 -37
  60. package/dist/setup.d.ts +24 -3
  61. package/dist/setup.js +117 -31
  62. package/dist/spawn-async.d.ts +18 -0
  63. package/dist/spawn-async.js +35 -0
  64. package/dist/stack.d.ts +14 -0
  65. package/dist/stack.js +44 -2
  66. package/dist/turri-versions.d.ts +71 -0
  67. package/dist/turri-versions.js +275 -0
  68. package/docs/README.md +2 -0
  69. package/docs/claude-subscription.md +131 -0
  70. package/docs/config.md +5 -0
  71. package/docs/deploy.md +2 -2
  72. package/docs/hosting.md +42 -4
  73. package/docs/installation.md +39 -2
  74. package/docs/parser.md +1 -1
  75. package/docs/regenerating.md +1 -1
  76. package/docs/sandbox.md +2 -1
  77. package/docs/sentinel.md +11 -3
  78. package/docs/using.md +4 -2
  79. package/genome/rules/turri-regeneration.md +1 -1
  80. package/package.json +1 -1
package/dist/setup.js CHANGED
@@ -1,18 +1,21 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { spawnAsync } from "./spawn-async.js";
2
3
  import { existsSync, rmSync, statSync } from "node:fs";
3
4
  import { join, relative, resolve } from "node:path";
4
5
  import { createInterface } from "node:readline/promises";
5
- import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
6
- import { globalExtensionFile, MIN_NODE, nodeVersionOk, PACKAGE_ROOT, turriOnPath } from "./environment.js";
7
- import { sandboxAvailable, sandboxWith } from "./regenerate/os-sandbox.js";
6
+ import { ModelRegistry } from "@earendil-works/pi-coding-agent";
7
+ import { EXTENSION_PROVIDERS, installedProviders, runtimeWithProviders } from "./installed-providers.js";
8
+ import { globalExtensionFile, MIN_NODE, nodeVersionOk, PACKAGE_ROOT, turriIdentity } from "./environment.js";
9
+ import { fetchLatest, newerThan, turrisOnPath, versionFindings } from "./turri-versions.js";
10
+ import { sandboxAvailable, sandboxWithAsync } from "./regenerate/os-sandbox.js";
8
11
  import { findTestScripts, onThisMachine } from "./regenerate/test-scripts.js";
9
12
  import { configKeyOptions, parseArgs } from "./cli-options.js";
10
13
  import { CONFIG_FILE, hasProjectConfig, readProjectConfig, stackConfigFile, stackOf, stacksOf } from "./config.js";
11
14
  import { checkModels, modelsInUse, piModelAccess } from "./model-check.js";
12
15
  import { bedrockRegionFor, describeBedrockRegion, regionInGeography } from "./bedrock-region.js";
13
- import { baseFiles } from "./regenerate/base-folder.js";
16
+ import { baseFilesAsync } from "./regenerate/base-folder.js";
14
17
  import { brand } from "./brand.js";
15
- import { readScript, scriptAgainstMachine, stackOnMachine } from "./stack.js";
18
+ import { readScript, scriptAgainstMachine, stackOnMachineAsync } from "./stack.js";
16
19
  const MARK = { ok: "✓", missing: "✗", limited: "!", info: "·" };
17
20
  function checkNode() {
18
21
  const version = process.versions.node;
@@ -66,9 +69,9 @@ export function checkSandbox(wall = sandboxAvailable(), platform = process.platf
66
69
  * testing script against this machine - a program it names that is not here, a toolchain it runs that is not on the
67
70
  * PATH. Nothing outside a project.
68
71
  */
69
- export function checkProject(project, machine = {}) {
72
+ export async function checkProject(project, machine = {}) {
70
73
  const checks = [];
71
- for (const { module, needs, missing } of stackOnMachine(project, machine.has, machine.jdk)) {
74
+ for (const { module, needs, missing } of await stackOnMachineAsync(project, machine.has, machine.jdk)) {
72
75
  if (needs.length === 0)
73
76
  continue;
74
77
  checks.push(missing.length === 0
@@ -99,9 +102,10 @@ export function checkProject(project, machine = {}) {
99
102
  }
100
103
  return checks;
101
104
  }
102
- async function checkProvider() {
105
+ async function checkProvider(project = process.cwd()) {
103
106
  try {
104
- const runtime = await ModelRuntime.create();
107
+ // pi's providers, and those the person's installed pi extensions register - a Claude subscription through Claude Code.
108
+ const runtime = await runtimeWithProviders(await installedProviders(project));
105
109
  const available = new ModelRegistry(runtime).getAvailable();
106
110
  const providers = [...new Set(available.map((model) => model.provider))];
107
111
  if (providers.length > 0) {
@@ -118,17 +122,46 @@ async function checkProvider() {
118
122
  fix: "run `pi` and use /login, or set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, ...)",
119
123
  };
120
124
  }
121
- function checkPath() {
122
- const found = turriOnPath();
123
- return found
124
- ? { level: "ok", name: "turri on PATH", detail: found }
125
- : {
126
- level: "limited",
127
- name: "turri on PATH",
128
- detail: "not found",
129
- // A clone has its sources; the package from npm has only what was compiled from them.
130
- fix: existsSync(join(PACKAGE_ROOT, "src")) ? `cd ${PACKAGE_ROOT} && npm link` : "npm install -g @turri-dev/cli",
131
- };
125
+ /**
126
+ * turri itself: whether it is the newest npm has - asked now - and every turri on the PATH, which runs, and what hides a
127
+ * newer one behind an older (turri-versions.ts). What to do, said under each.
128
+ */
129
+ async function checkTurri() {
130
+ const own = turriIdentity();
131
+ const onPath = turrisOnPath();
132
+ const latest = own.git_commit === null ? await fetchLatest() : undefined;
133
+ const findings = versionFindings({ ...(latest !== undefined ? { latest } : {}), onPath });
134
+ const version = own.version ?? "?";
135
+ const outdated = findings.find((finding) => finding.kind === "outdated");
136
+ const versionCheck = own.git_commit !== null
137
+ ? { level: "info", name: "turri version", detail: `${version}, from a clone - not compared with npm` }
138
+ : outdated !== undefined
139
+ ? { level: "limited", name: "turri version", detail: outdated.text, fix: outdated.fixes.join("; ") }
140
+ : latest === undefined
141
+ ? { level: "info", name: "turri version", detail: `${version} - npm did not answer, so it is not compared` }
142
+ : own.version !== null && newerThan(latest, own.version)
143
+ ? // Older than npm's, with a newer turri installed already: the PATH check says which, and what hides it.
144
+ { level: "limited", name: "turri version", detail: `${version} - npm has ${latest}, and a newer turri is installed already: see turri on PATH` }
145
+ : { level: "ok", name: "turri version", detail: `${version} - the newest on npm` };
146
+ if (onPath.length === 0) {
147
+ return [
148
+ versionCheck,
149
+ {
150
+ level: "limited",
151
+ name: "turri on PATH",
152
+ detail: "not found",
153
+ // A clone has its sources; the package from npm has only what was compiled from them.
154
+ fix: existsSync(join(PACKAGE_ROOT, "src")) ? `cd ${PACKAGE_ROOT} && npm link` : "npm install -g @turri-dev/cli",
155
+ },
156
+ ];
157
+ }
158
+ const copies = onPath.map((turri) => `${turri.path} turri ${turri.version ?? "?"} (${[turri.runs ? "runs" : "hidden", ...(turri.self ? ["this one"] : []), ...(turri.clone ? ["a clone"] : [])].join(", ")})`);
159
+ const hidden = findings.filter((finding) => finding.kind !== "outdated");
160
+ const pathCheck = hidden.length > 0
161
+ ? // Every turri, then what to do, in order: a step a line.
162
+ { level: "missing", name: "turri on PATH", detail: hidden[0].text, more: [...copies, ...hidden.flatMap((finding) => finding.fixes).map((fix) => `→ ${fix}`)] }
163
+ : { level: "ok", name: "turri on PATH", detail: onPath.length === 1 ? onPath[0].path : `${onPath.length} turris - the first, which runs, is the newest`, ...(onPath.length > 1 ? { more: copies } : {}) };
164
+ return [versionCheck, pathCheck];
132
165
  }
133
166
  /**
134
167
  * turri loaded into every `pi` - a file an older `turri setup` wrote into pi's extensions. turri and pi are kept apart
@@ -175,9 +208,9 @@ export async function checkProjectModels(project, access) {
175
208
  function asksModels(options) {
176
209
  return options.models === true && options.dir !== undefined && (hasProjectConfig(options.dir) || options.modelsGiven === true);
177
210
  }
178
- /** Whether `command` runs with `env` - its PATH looked in, as the agents' shells and the testing scripts would. */
179
- function runs(command, env) {
180
- return spawnSync(command, ["--version"], { stdio: "ignore", windowsHide: true, env }).status === 0;
211
+ /** Whether `command` runs with `env` - its PATH looked in, as the agents' shells and the testing scripts would. Never holds a host's thread up. */
212
+ async function runs(command, env) {
213
+ return (await spawnAsync(command, ["--version"], { env })).status === 0;
181
214
  }
182
215
  /**
183
216
  * Whether this machine - and, with `dir`, that project - is ready for turri, as checks. It prints nothing and asks
@@ -188,41 +221,94 @@ export async function doctor(options = {}) {
188
221
  }
189
222
  async function allChecks(options) {
190
223
  const env = options.env ?? process.env;
191
- const checks = [checkNode(), checkShell(runs("bash", env)), checkGit(runs("git", env)), checkSandbox(sandboxWith(env)), await checkProvider()];
224
+ // Each check that runs a command runs it without holding the thread up - a host asks on its own - side by side.
225
+ const [bash, git, wall, provider] = await Promise.all([runs("bash", env), runs("git", env), sandboxWithAsync(env), checkProvider(options.dir)]);
226
+ const checks = [checkNode(), checkShell(bash), checkGit(git), checkSandbox(wall), provider];
192
227
  if (options.cli === true)
193
- checks.push(checkPath(), ...checkGlobal());
228
+ checks.push(...(await checkTurri()), ...checkGlobal());
194
229
  // In a project: what it needs of this machine, and whether its models answer.
195
230
  const project = options.dir;
196
231
  if (project === undefined)
197
232
  return checks;
198
233
  if (hasProjectConfig(project) || existsSync(join(project, "plain_modules")))
199
- checks.push(...checkProject(project, { has: (command) => onThisMachine(command, env) !== undefined }));
234
+ checks.push(...(await checkProject(project, { has: (command) => onThisMachine(command, env) !== undefined })));
200
235
  if (hasProjectConfig(project))
201
- checks.push(...checkBedrockRegion(project, env), ...checkBaseFolders(project));
236
+ checks.push(...checkBedrockRegion(project, env), ...(await checkClaudeSubscription(project, env)), ...(await checkBaseFolders(project)));
202
237
  if (asksModels(options))
203
238
  checks.push(...(await checkProjectModels(project)));
204
239
  return checks;
205
240
  }
241
+ /** The environment pi-claude-code refuses to run with: each points Claude Code at an API key or another backend, not the subscription. */
242
+ export const CLAUDE_CODE_CONFLICTS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_FOUNDRY_API_KEY"];
243
+ const CLAUDE_CODE_BACKENDS = ["CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"];
244
+ const claudeCodeProbe = (project) => ({
245
+ installed: async () => (await installedProviders(project))?.getRegisteredProviderIds().includes("claude-code") === true,
246
+ async status(command, env) {
247
+ const run = await spawnAsync(command, ["auth", "status"], { env, timeout: 15_000 });
248
+ if (run.error !== undefined)
249
+ return undefined;
250
+ try {
251
+ return JSON.parse(run.stdout);
252
+ }
253
+ catch {
254
+ return { loggedIn: false };
255
+ }
256
+ },
257
+ which: (command, env) => (command.includes("/") ? (existsSync(command) ? command : undefined) : onThisMachine(command, env)),
258
+ });
259
+ /**
260
+ * A Claude subscription through Claude Code, when the project's config names a `claude-code/...` model: everything
261
+ * pi-claude-code needs, in the order it needs it - its extension installed, Claude Code on the PATH, signed in to a Pro or
262
+ * Max subscription, and nothing in the environment it refuses. Nothing when no model is a claude-code one.
263
+ */
264
+ export async function checkClaudeSubscription(project, env = process.env, probe = claudeCodeProbe(project)) {
265
+ const configs = [readProjectConfig(project), ...stacksOf(project).map((stack) => readProjectConfig(project, stack))];
266
+ const named = [...new Set(configs.flatMap((config) => modelsInUse(config)).filter((used) => used.spec.startsWith("claude-code/")).map((used) => used.spec))];
267
+ if (named.length === 0)
268
+ return [];
269
+ const name = "Claude subscription";
270
+ const extension = EXTENSION_PROVIDERS["claude-code"];
271
+ if (!(await probe.installed())) {
272
+ return [{ level: "missing", name, detail: `${named.join(", ")} need pi-claude-code, pi's extension for a Claude subscription, which is not installed`, fix: extension.install }];
273
+ }
274
+ const command = env.PI_CLAUDE_CODE_COMMAND?.trim() || "claude";
275
+ const found = probe.which(command, env);
276
+ if (found === undefined) {
277
+ return [{ level: "missing", name, detail: `Claude Code (${command}) is not on the PATH - every claude-code request goes through it`, fix: "npm install -g @anthropic-ai/claude-code - or name it in PI_CLAUDE_CODE_COMMAND" }];
278
+ }
279
+ const configDir = env.PI_CLAUDE_CODE_CONFIG_DIR?.trim();
280
+ const status = await probe.status(found, configDir ? { ...env, CLAUDE_CONFIG_DIR: configDir } : env);
281
+ if (status?.loggedIn !== true)
282
+ return [{ level: "missing", name, detail: `Claude Code (${found}) is not signed in`, fix: "claude auth login - with the Claude account that has the Pro or Max subscription" }];
283
+ if (status.subscriptionType === undefined || status.subscriptionType === "") {
284
+ return [{ level: "missing", name, detail: `Claude Code is signed in${status.authMethod !== undefined ? ` (${status.authMethod})` : ""}, but not to a Claude subscription`, fix: "claude auth logout, then claude auth login with the account that has the Pro or Max subscription" }];
285
+ }
286
+ const conflicts = [...CLAUDE_CODE_CONFLICTS.filter((key) => (env[key] ?? "") !== ""), ...CLAUDE_CODE_BACKENDS.filter((key) => /^(1|true|yes|on)$/i.test(env[key]?.trim() ?? ""))];
287
+ if (conflicts.length > 0) {
288
+ return [{ level: "missing", name, detail: `${conflicts.join(", ")} ${conflicts.length === 1 ? "is" : "are"} set: pi-claude-code refuses to run with ${conflicts.length === 1 ? "it" : "them"}, as ${conflicts.length === 1 ? "it points" : "they point"} Claude Code away from the subscription`, fix: `unset ${conflicts.join(" ")} where turri starts - your shell's profile, say` }];
289
+ }
290
+ return [{ level: "ok", name, detail: `pi-claude-code installed, Claude Code signed in to ${status.subscriptionType} - for ${named.join(", ")}` }];
291
+ }
206
292
  /**
207
293
  * The base folders the project's config names - its own, and each stack's - each there with its files, or not: a codebase
208
294
  * that requires nothing starts from one, and its first pulse stops when it is not there. Nothing when none is named.
209
295
  */
210
- export function checkBaseFolders(project) {
296
+ export async function checkBaseFolders(project) {
211
297
  const named = new Map();
212
298
  for (const [stack, config] of [[undefined, readProjectConfig(project)], ...stacksOf(project).map((stack) => [stack, readProjectConfig(project, stack)])]) {
213
299
  if (config.baseFolder === undefined)
214
300
  continue;
215
301
  named.set(config.baseFolder, [...(named.get(config.baseFolder) ?? []), stack === undefined ? CONFIG_FILE : stackConfigFile(stack)]);
216
302
  }
217
- return [...named].map(([folder, files]) => {
303
+ return Promise.all([...named].map(async ([folder, files]) => {
218
304
  const path = resolve(project, folder);
219
305
  const name = `base folder ${folder}`;
220
306
  if (!existsSync(path) || !statSync(path).isDirectory()) {
221
307
  return { level: "missing", name, detail: `${existsSync(path) ? "not a folder" : "not there"} - named in ${files.join(", ")}`, fix: "make the folder, or name the right one in base-folder:, by path from the project's folder" };
222
308
  }
223
- const count = baseFiles(path).length;
309
+ const count = (await baseFilesAsync(path)).length;
224
310
  return { level: "ok", name, detail: `${count} file${count === 1 ? "" : "s"} to start a codebase from - named in ${files.join(", ")}` };
225
- });
311
+ }));
226
312
  }
227
313
  /**
228
314
  * The AWS region a project's Bedrock models are called in, when it uses any: turri's, the person's or the profile's - and
@@ -0,0 +1,18 @@
1
+ /** A command's run, as `spawnSync` gives it - its exit status (null when it did not exit), its output, what kept it from starting. */
2
+ export interface AsyncRun {
3
+ status: number | null;
4
+ stdout: string;
5
+ stderr: string;
6
+ error?: Error;
7
+ }
8
+ /**
9
+ * A command run without holding the thread up - `spawnSync`'s answer, awaited. For what a host asks on its own thread
10
+ * (doctor): a check never keeps the host from answering meanwhile. `timeout` kills it after that many milliseconds.
11
+ */
12
+ export declare function spawnAsync(command: string, args: string[], options?: {
13
+ env?: NodeJS.ProcessEnv;
14
+ timeout?: number;
15
+ cwd?: string;
16
+ }): Promise<AsyncRun>;
17
+ /** The host's turn: whatever waits on its thread runs before the next step of a long piece of work. */
18
+ export declare const yieldToHost: () => Promise<void>;
@@ -0,0 +1,35 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * A command run without holding the thread up - `spawnSync`'s answer, awaited. For what a host asks on its own thread
4
+ * (doctor): a check never keeps the host from answering meanwhile. `timeout` kills it after that many milliseconds.
5
+ */
6
+ export function spawnAsync(command, args, options = {}) {
7
+ return new Promise((resolve) => {
8
+ let stdout = "";
9
+ let stderr = "";
10
+ let settled = false;
11
+ const done = (run) => {
12
+ if (settled)
13
+ return;
14
+ settled = true;
15
+ if (timer !== undefined)
16
+ clearTimeout(timer);
17
+ resolve(run);
18
+ };
19
+ let child;
20
+ try {
21
+ child = spawn(command, args, { env: options.env, cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
22
+ }
23
+ catch (error) {
24
+ done({ status: null, stdout, stderr, error: error instanceof Error ? error : new Error(String(error)) });
25
+ return;
26
+ }
27
+ const timer = options.timeout !== undefined ? setTimeout(() => child.kill("SIGKILL"), options.timeout) : undefined;
28
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
29
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
30
+ child.on("error", (error) => done({ status: null, stdout, stderr, error }));
31
+ child.on("close", (status) => done({ status, stdout, stderr }));
32
+ });
33
+ }
34
+ /** The host's turn: whatever waits on its thread runs before the next step of a long piece of work. */
35
+ export const yieldToHost = () => new Promise((resolve) => setImmediate(resolve));
package/dist/stack.d.ts CHANGED
@@ -19,6 +19,14 @@ export declare function projectImplementationReqs(project: string): Array<{
19
19
  module: string;
20
20
  reqs: string[];
21
21
  }>;
22
+ /**
23
+ * `projectImplementationReqs`, giving the host's thread back between modules: a host - doctor on its own thread - answers
24
+ * meanwhile. Each module is parsed as `plainFileParser` keeps it, so the chain below one parsed already is not again.
25
+ */
26
+ export declare function projectImplementationReqsAsync(project: string): Promise<Array<{
27
+ module: string;
28
+ reqs: string[];
29
+ }>>;
22
30
  /**
23
31
  * What a project's modules need of this machine, and which of it the machine lacks - `has` finding a command, and on
24
32
  * macOS a JDK behind `java` (`toolchainCheck`), asked at most once.
@@ -28,6 +36,12 @@ export declare function stackOnMachine(project: string, has?: (command: string)
28
36
  needs: StackNeed[];
29
37
  missing: StackNeed[];
30
38
  }>;
39
+ /** `stackOnMachine`, never holding the host's thread up: the modules parsed one a turn, a JDK looked for without blocking. */
40
+ export declare function stackOnMachineAsync(project: string, has?: (command: string) => boolean, jdk?: () => boolean | Promise<boolean>): Promise<Array<{
41
+ module: string;
42
+ needs: StackNeed[];
43
+ missing: StackNeed[];
44
+ }>>;
31
45
  /**
32
46
  * The standard library's languages, and what builds and tests their stacks here - every command needed. Python's stacks
33
47
  * run through uv, which fetches Python itself; TypeScript's through npm; Java's through the Maven or Gradle wrapper,
package/dist/stack.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { spawnAsync, yieldToHost } from "./spawn-async.js";
2
3
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
4
  import { basename, join } from "node:path";
4
5
  import { plainFileParser, withParseCache } from "./plain/parser.js";
@@ -55,6 +56,32 @@ export function projectImplementationReqs(project) {
55
56
  return modules;
56
57
  });
57
58
  }
59
+ /**
60
+ * `projectImplementationReqs`, giving the host's thread back between modules: a host - doctor on its own thread - answers
61
+ * meanwhile. Each module is parsed as `plainFileParser` keeps it, so the chain below one parsed already is not again.
62
+ */
63
+ export async function projectImplementationReqsAsync(project) {
64
+ let files;
65
+ try {
66
+ files = readdirSync(project).filter((name) => name.endsWith(".plain"));
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ const modules = [];
72
+ for (const name of files) {
73
+ await yieldToHost();
74
+ const path = join(project, name);
75
+ try {
76
+ const [module, tree] = plainFileParser(basename(path), resolveTemplateDirs(path), { allowNoFunctionalities: true });
77
+ modules.push({ module, reqs: sectionsOf(tree)["implementation reqs"] });
78
+ }
79
+ catch {
80
+ // As projectImplementationReqs: nothing named here.
81
+ }
82
+ }
83
+ return modules;
84
+ }
58
85
  /** Whether a command is on this machine's PATH. */
59
86
  const onPath = (command) => onThisMachine(command) !== undefined;
60
87
  /**
@@ -63,8 +90,19 @@ const onPath = (command) => onThisMachine(command) !== undefined;
63
90
  */
64
91
  export function stackOnMachine(project, has = onPath, jdk = jdkHere) {
65
92
  let found;
66
- const here = toolchainCheck(has, () => (found ??= jdk()));
67
- return projectImplementationReqs(project).map(({ module, reqs }) => {
93
+ return needsAgainst(projectImplementationReqs(project), toolchainCheck(has, () => (found ??= jdk())));
94
+ }
95
+ /** `stackOnMachine`, never holding the host's thread up: the modules parsed one a turn, a JDK looked for without blocking. */
96
+ export async function stackOnMachineAsync(project, has = onPath, jdk = jdkHereAsync) {
97
+ const modules = await projectImplementationReqsAsync(project);
98
+ // Asked only when a module needs Java and java is on the PATH - as stackOnMachine asks it, at most once.
99
+ let found = false;
100
+ if (modules.some(({ reqs }) => stackOf(reqs.join("\n")).some((need) => need.commands.includes("java"))) && has("java"))
101
+ found = await jdk();
102
+ return needsAgainst(modules, toolchainCheck(has, () => found));
103
+ }
104
+ function needsAgainst(modules, here) {
105
+ return modules.map(({ module, reqs }) => {
68
106
  const needs = stackOf(reqs.join("\n"));
69
107
  return { module, needs, missing: needs.filter((need) => !need.commands.some(here)) };
70
108
  });
@@ -81,6 +119,10 @@ export const STDLIB_LANGUAGES = [
81
119
  { language: "Rust", commands: ["cargo"] },
82
120
  { language: "Java", commands: ["java"] },
83
121
  ];
122
+ /** `jdkHere`, without holding the thread up. */
123
+ async function jdkHereAsync() {
124
+ return process.platform !== "darwin" || (await spawnAsync("/usr/libexec/java_home", [])).status === 0;
125
+ }
84
126
  /** Whether this machine has a JDK behind `java`: anywhere but macOS, java found says so; on macOS, `java_home` finding one. */
85
127
  function jdkHere() {
86
128
  return process.platform !== "darwin" || spawnSync("/usr/libexec/java_home", [], { stdio: "ignore" }).status === 0;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Which turri runs, which is newest, and which others are on this machine - so a person who installed a new turri and
3
+ * is still running an old one is told so, and how to fix it, where they look: the banner as turri starts (from what the
4
+ * last check found, never waiting on the network), and `turri --version` and `turri doctor` (asking npm then and there).
5
+ * A turri run from a clone is the developer's: it is never told to update.
6
+ */
7
+ export declare const PACKAGE_NAME = "@turri-dev/cli";
8
+ /** Whether version `a` is newer than `b`: major, minor, patch - and a release over a prerelease of the same. */
9
+ export declare function newerThan(a: string, b: string): boolean;
10
+ /** The newest version npm had when it was last asked, and when - nothing when it never was. */
11
+ export declare function latestKnown(): {
12
+ version: string;
13
+ checkedAt: number;
14
+ } | undefined;
15
+ /** The newest version npm has now, kept for the next start - nothing when npm does not answer in time. */
16
+ export declare function fetchLatest(timeoutMs?: number): Promise<string | undefined>;
17
+ /**
18
+ * Ask npm again in the background when what it said last is old: what it says now, once it answers - undefined when it
19
+ * was not asked, or did not answer. Never waited for by what starts turri.
20
+ */
21
+ export declare function refreshLatestLater(): Promise<string | undefined>;
22
+ /** A turri on this machine's PATH. */
23
+ export interface TurriOnPath {
24
+ /** The command, as the PATH has it. */
25
+ path: string;
26
+ /** The package it runs. */
27
+ root: string;
28
+ version?: string;
29
+ /** Run from a clone - npm link, or a script pointing into one. */
30
+ clone: boolean;
31
+ /** The one a bare `turri` runs: the first on the PATH. */
32
+ runs: boolean;
33
+ /** The one running now. */
34
+ self: boolean;
35
+ }
36
+ /** Every turri on the PATH, in its order - the first is what a bare `turri` runs. */
37
+ export declare function turrisOnPath(path?: string, self?: string, platform?: NodeJS.Platform): TurriOnPath[];
38
+ /** How to take a turri off the PATH, by where it is: another Node version of nvm, a linked clone, or an npm prefix. */
39
+ export declare function removalOf(turri: Pick<TurriOnPath, "path" | "clone" | "root">, platform?: NodeJS.Platform): string;
40
+ /** What is wrong with the turri running, and what to do: each a line, with its fixes. */
41
+ export interface VersionFinding {
42
+ /** Updating is all it needs. */
43
+ kind: "outdated" | "shadowed" | "behind";
44
+ text: string;
45
+ /** The same, as the banner says it in a line: what is newer, and what to run. */
46
+ short: string;
47
+ fixes: string[];
48
+ }
49
+ /**
50
+ * What the turri running, the newest npm has (`latest`, when known) and the turris on the PATH say together: nothing when
51
+ * all is well. A turri from a clone is told nothing of npm's.
52
+ */
53
+ export declare function versionFindings(options?: {
54
+ latest?: string;
55
+ onPath?: TurriOnPath[];
56
+ self?: {
57
+ version?: string;
58
+ clone: boolean;
59
+ root: string;
60
+ };
61
+ }): VersionFinding[];
62
+ /** `turri --version`: the version line, then where this turri is, what npm has - asked now - and every turri on the PATH. */
63
+ export declare function describeVersion(versionLine: string): Promise<string>;
64
+ /**
65
+ * What the banner says as turri starts: a line a finding, short - what is newer and what to run - from what npm said at
66
+ * the last check and the turris on the PATH, never waiting on the network; the check for next time started behind it.
67
+ */
68
+ export declare function bannerWarnings(): {
69
+ lines: string[];
70
+ later: Promise<string | undefined>;
71
+ };