@vimhead.dev/pi-norn 0.1.0-tip.36148588553.1 → 0.1.0-tip.36679386408.1

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 (3) hide show
  1. package/README.md +7 -3
  2. package/dist/index.js +142 -14
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -10,9 +10,13 @@ pi install npm:@vimhead.dev/pi-norn@tip
10
10
  pi
11
11
  ```
12
12
 
13
- The adapter selects `norn` from `PATH`, or `pi --norn-executable /absolute/path/to/norn`.
13
+ Use [project-local runtime selection](https://github.com/vimhead/norn#project-local-runtime-selection)
14
+ with `.nornrc.json` to launch an installed CLI dependency without global Norn.
15
+ `pi --norn-executable /absolute/path/to/norn` overrides project selection;
16
+ without either, the adapter selects `norn` from `PATH`.
14
17
  It does not carry another Norn runtime or SDK and does not inject authoring context
15
- into Norn-managed agent sessions. Workflow discovery is skipped in untrusted
16
- projects. Both introductions refresh at session start or `/reload`.
18
+ into Norn-managed agent sessions. Repository runtime configuration and workflow
19
+ discovery are skipped in untrusted projects. Both introductions refresh at session
20
+ start or `/reload`.
17
21
 
18
22
  [Runtime installation](https://github.com/vimhead/norn#installation).
package/dist/index.js CHANGED
@@ -1,6 +1,121 @@
1
1
  // ../core/src/agent-protocol.ts
2
2
  var AGENT_RESPONSE_TOOL_NAME = "pi_workflows_agent_response";
3
3
 
4
+ // ../core/src/runtime-resolution.mjs
5
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
6
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
7
+ var CLI_PACKAGE_NAME = "@vimhead.dev/norn-cli";
8
+ var RuntimeResolutionError = class extends Error {
9
+ };
10
+ async function resolveNornRuntime({
11
+ cwd,
12
+ executableOverride,
13
+ isProjectTrusted,
14
+ nodeExecutable
15
+ }) {
16
+ if (executableOverride !== null) {
17
+ if (typeof executableOverride !== "string" || !executableOverride.trim())
18
+ throw new RuntimeResolutionError(
19
+ "Norn executable override must be a nonempty executable path or name."
20
+ );
21
+ return { executable: executableOverride, args: [] };
22
+ }
23
+ if (!isProjectTrusted) return { executable: "norn", args: [] };
24
+ const configPath = await findAncestorEntry({
25
+ cwd,
26
+ relativePath: ".nornrc.json"
27
+ });
28
+ if (configPath === null) return { executable: "norn", args: [] };
29
+ const packageRoot = await readRuntimePackageRoot(configPath);
30
+ const scriptPath = await resolvePackageEntrypoint(packageRoot);
31
+ return { executable: nodeExecutable, args: [scriptPath] };
32
+ }
33
+ async function readRuntimePackageRoot(configPath) {
34
+ const configuration = await readManifest(configPath);
35
+ if (!isRecord(configuration) || Object.keys(configuration).some((key) => key !== "runtime") || !isRecord(configuration.runtime) || Object.keys(configuration.runtime).some((key) => key !== "packageRoot") || typeof configuration.runtime.packageRoot !== "string" || !configuration.runtime.packageRoot.trim())
36
+ throw new RuntimeResolutionError(
37
+ `Invalid ${JSON.stringify(configPath)}: expected { "runtime": { "packageRoot": "./path/to/package" } }.`
38
+ );
39
+ return resolve(dirname(configPath), configuration.runtime.packageRoot);
40
+ }
41
+ async function resolvePackageEntrypoint(packageRoot) {
42
+ const owner = await readManifest(join(packageRoot, "package.json"));
43
+ if (!isRecord(owner) || ![
44
+ owner.dependencies,
45
+ owner.devDependencies,
46
+ owner.optionalDependencies
47
+ ].some(
48
+ (dependencies) => isRecord(dependencies) && typeof dependencies[CLI_PACKAGE_NAME] === "string"
49
+ ))
50
+ throw new RuntimeResolutionError(
51
+ `The package at ${JSON.stringify(packageRoot)} must declare ${CLI_PACKAGE_NAME} as a dependency or devDependency.`
52
+ );
53
+ const packagePath = await findAncestorEntry({
54
+ cwd: packageRoot,
55
+ relativePath: `node_modules/${CLI_PACKAGE_NAME}`
56
+ });
57
+ if (packagePath === null)
58
+ throw new RuntimeResolutionError(
59
+ `Cannot find ${CLI_PACKAGE_NAME} from ${JSON.stringify(packageRoot)}. Install that package's dependencies and retry.`
60
+ );
61
+ const manifestPath = join(packagePath, "package.json");
62
+ const installedManifestPath = await realpath(manifestPath).catch(() => {
63
+ throw new RuntimeResolutionError(
64
+ `Cannot resolve the installed CLI at ${JSON.stringify(manifestPath)}. Reinstall the selected package's dependencies.`
65
+ );
66
+ });
67
+ const installed = await readManifest(installedManifestPath);
68
+ const entrypoint = isRecord(installed?.bin) ? installed.bin.norn : void 0;
69
+ if (installed?.name !== CLI_PACKAGE_NAME || typeof entrypoint !== "string" || !entrypoint.trim() || isAbsolute(entrypoint))
70
+ throw new RuntimeResolutionError(
71
+ `Invalid Norn CLI manifest at ${JSON.stringify(installedManifestPath)}: expected a relative bin.norn entrypoint.`
72
+ );
73
+ const installationRoot = dirname(installedManifestPath);
74
+ const scriptPath = resolve(installationRoot, entrypoint);
75
+ if (!scriptPath.startsWith(`${installationRoot}${sep}`))
76
+ throw new RuntimeResolutionError(
77
+ `Norn CLI entrypoint must be inside ${JSON.stringify(installationRoot)}.`
78
+ );
79
+ try {
80
+ if (!(await stat(scriptPath)).isFile()) throw new Error();
81
+ } catch {
82
+ throw new RuntimeResolutionError(
83
+ `Missing Norn CLI entrypoint at ${JSON.stringify(scriptPath)}. Reinstall the selected package's dependencies.`
84
+ );
85
+ }
86
+ return scriptPath;
87
+ }
88
+ function isRecord(value) {
89
+ return value !== null && typeof value === "object" && !Array.isArray(value);
90
+ }
91
+ async function readManifest(path) {
92
+ try {
93
+ return JSON.parse(await readFile(path, "utf8"));
94
+ } catch {
95
+ throw new RuntimeResolutionError(
96
+ `Cannot read valid JSON from ${JSON.stringify(path)}. Check the selected package and configuration.`
97
+ );
98
+ }
99
+ }
100
+ async function findAncestorEntry({ cwd, relativePath }) {
101
+ let directory = resolve(cwd);
102
+ while (true) {
103
+ const candidate = join(directory, relativePath);
104
+ try {
105
+ await lstat(candidate);
106
+ return candidate;
107
+ } catch (error) {
108
+ if (error.code !== "ENOENT")
109
+ throw new RuntimeResolutionError(
110
+ `Cannot inspect ${JSON.stringify(candidate)}.`
111
+ );
112
+ }
113
+ const parent = dirname(directory);
114
+ if (parent === directory) return null;
115
+ directory = parent;
116
+ }
117
+ }
118
+
4
119
  // src/index.ts
5
120
  var INTRO_START = "<norn-docs-intro>";
6
121
  var INTRO_END = "</norn-docs-intro>";
@@ -8,7 +123,7 @@ function nornPiAdapter(pi) {
8
123
  let cachedIntro;
9
124
  pi.registerFlag("norn-executable", {
10
125
  type: "string",
11
- description: "Norn executable for docs intro and workflows intro (defaults to norn on PATH). Accepts an executable path, not a shell command."
126
+ description: "Norn executable override for docs intro and workflows intro (otherwise uses trusted .nornrc.json, then norn on PATH). Accepts an executable path, not a shell command."
12
127
  });
13
128
  pi.on("session_start", async (_event, ctx) => {
14
129
  cachedIntro = void 0;
@@ -18,20 +133,32 @@ function nornPiAdapter(pi) {
18
133
  const configuredExecutable = pi.getFlag("norn-executable");
19
134
  if (configuredExecutable !== void 0 && typeof configuredExecutable !== "string")
20
135
  throw new Error("Invalid Norn executable flag");
21
- const executable = configuredExecutable ?? "norn";
136
+ const isProjectTrusted = ctx.isProjectTrusted();
137
+ const invocation = await resolveNornRuntime({
138
+ cwd: ctx.cwd,
139
+ executableOverride: configuredExecutable ?? null,
140
+ isProjectTrusted,
141
+ nodeExecutable: process.versions.bun ? "node" : process.execPath
142
+ });
22
143
  const [intro, workflowsIntro] = await Promise.all([
23
- loadIntroduction({ pi, executable, cwd: ctx.cwd, group: "docs" }),
24
- ctx.isProjectTrusted() ? loadIntroduction({
144
+ loadIntroduction({ pi, invocation, cwd: ctx.cwd, group: "docs" }),
145
+ isProjectTrusted ? loadIntroduction({
25
146
  pi,
26
- executable,
147
+ invocation,
27
148
  cwd: ctx.cwd,
28
149
  group: "workflows"
29
150
  }) : Promise.resolve("")
30
151
  ]);
31
- cachedIntro = { executable, intro, workflowsIntro };
32
- } catch {
152
+ cachedIntro = {
153
+ configuredExecutable,
154
+ cwd: ctx.cwd,
155
+ isProjectTrusted,
156
+ intro,
157
+ workflowsIntro
158
+ };
159
+ } catch (error) {
33
160
  ctx.ui.notify(
34
- "Norn introduction unavailable. Check --norn-executable and run that executable with 'docs intro' and 'workflows intro' in the project directory to diagnose, then /reload to retry.",
161
+ `Norn introduction unavailable.${error instanceof RuntimeResolutionError ? ` ${error.message}` : ""} Check --norn-executable or .nornrc.json and run the selected runtime with 'docs intro' and 'workflows intro' in the project directory to diagnose, then /reload to retry.`,
35
162
  "warning"
36
163
  );
37
164
  }
@@ -41,10 +168,10 @@ function nornPiAdapter(pi) {
41
168
  if (pi.getAllTools().some((tool) => tool.name === AGENT_RESPONSE_TOOL_NAME))
42
169
  return;
43
170
  if (event.systemPrompt.includes(INTRO_START)) return;
44
- if (cachedIntro.executable !== (pi.getFlag("norn-executable") ?? "norn")) {
171
+ if (cachedIntro.configuredExecutable !== pi.getFlag("norn-executable") || cachedIntro.cwd !== ctx.cwd || cachedIntro.isProjectTrusted !== ctx.isProjectTrusted()) {
45
172
  cachedIntro = void 0;
46
173
  ctx.ui.notify(
47
- "Norn executable changed. Run /reload to refresh its introduction.",
174
+ "Norn runtime selection or project trust changed. Run /reload to refresh its introduction.",
48
175
  "warning"
49
176
  );
50
177
  return;
@@ -61,10 +188,11 @@ ${cachedIntro.workflowsIntro}` : ""}`
61
188
  });
62
189
  }
63
190
  async function loadIntroduction(input) {
64
- const result = await input.pi.exec(input.executable, [input.group, "intro"], {
65
- cwd: input.cwd,
66
- timeout: 1e4
67
- });
191
+ const result = await input.pi.exec(
192
+ input.invocation.executable,
193
+ [...input.invocation.args, input.group, "intro"],
194
+ { cwd: input.cwd, timeout: 1e4 }
195
+ );
68
196
  if (result.killed || result.code !== 0)
69
197
  throw new Error(`Norn ${input.group} intro did not complete successfully`);
70
198
  const response = JSON.parse(result.stdout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vimhead.dev/pi-norn",
3
- "version": "0.1.0-tip.36148588553.1",
3
+ "version": "0.1.0-tip.36679386408.1",
4
4
  "description": "Deliver installed Norn runtime documentation to outer Pi agent sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@earendil-works/pi-coding-agent": "0.85.1",
41
+ "@earendil-works/pi-coding-agent": "0.99.1",
42
42
  "@vimhead.dev/norn-core": "0.0.0"
43
43
  },
44
44
  "scripts": {