runspec-node 0.22.0 → 0.23.0

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/dist/finder.d.ts CHANGED
@@ -1,4 +1,36 @@
1
+ /**
2
+ * Locate the governing `runspec.toml`.
3
+ *
4
+ * Resolution order mirrors the Python `parse()`:
5
+ * 1. `RUNSPEC_CONFIG` env var — explicit override.
6
+ * 2. an explicit `start` directory, when one is passed.
7
+ * 3. **caller-relative** — walk up from the entry script's directory
8
+ * (`require.main` / `process.argv[1]`). This is what lets an installed
9
+ * runnable find its own config from *any* working directory — e.g. when a
10
+ * controller launches `bin/greet` over SSH with `cwd` = the login home,
11
+ * not the package. Mirrors Python resolving relative to the installed
12
+ * package, "what makes installed entry points work from any working
13
+ * directory".
14
+ * 4. **cwd-relative** — walk up from `process.cwd()` (the historical
15
+ * behaviour, kept as the final fallback).
16
+ *
17
+ * A `runspec.toml` found *inside* a `node_modules` path is ignored: those
18
+ * belong to installed dependencies (including runspec-node's own bundled CLI
19
+ * spec), never the project being discovered.
20
+ */
1
21
  export declare function findConfig(start?: string): {
2
22
  configPath: string;
3
23
  };
24
+ /**
25
+ * Ordered, de-duplicated list of directories to walk up from, mirroring the
26
+ * resolution order documented on `findConfig`. Pure (no I/O) so the priority
27
+ * logic is unit-testable without a filesystem fixture.
28
+ */
29
+ export declare function searchStarts(start: string | undefined, entryDir: string | undefined, cwd: string): string[];
30
+ /**
31
+ * Walk up from `start` looking for a `runspec.toml`, skipping any found under a
32
+ * `node_modules` path (a dependency's spec, not the project's). Returns the
33
+ * path or null. Exported for testing.
34
+ */
35
+ export declare function walkUp(start: string): string | null;
4
36
  //# sourceMappingURL=finder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"finder.d.ts","sourceRoot":"","sources":["../src/finder.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAiBjE"}
1
+ {"version":3,"file":"finder.d.ts","sourceRoot":"","sources":["../src/finder.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAoBjE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAM3G;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWnD"}
package/dist/finder.js CHANGED
@@ -34,20 +34,81 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.findConfig = findConfig;
37
+ exports.searchStarts = searchStarts;
38
+ exports.walkUp = walkUp;
37
39
  const fs = __importStar(require("fs"));
38
40
  const path = __importStar(require("path"));
41
+ /**
42
+ * Locate the governing `runspec.toml`.
43
+ *
44
+ * Resolution order mirrors the Python `parse()`:
45
+ * 1. `RUNSPEC_CONFIG` env var — explicit override.
46
+ * 2. an explicit `start` directory, when one is passed.
47
+ * 3. **caller-relative** — walk up from the entry script's directory
48
+ * (`require.main` / `process.argv[1]`). This is what lets an installed
49
+ * runnable find its own config from *any* working directory — e.g. when a
50
+ * controller launches `bin/greet` over SSH with `cwd` = the login home,
51
+ * not the package. Mirrors Python resolving relative to the installed
52
+ * package, "what makes installed entry points work from any working
53
+ * directory".
54
+ * 4. **cwd-relative** — walk up from `process.cwd()` (the historical
55
+ * behaviour, kept as the final fallback).
56
+ *
57
+ * A `runspec.toml` found *inside* a `node_modules` path is ignored: those
58
+ * belong to installed dependencies (including runspec-node's own bundled CLI
59
+ * spec), never the project being discovered.
60
+ */
39
61
  function findConfig(start) {
40
- let dir = path.resolve(start ?? process.cwd());
62
+ const env = process.env.RUNSPEC_CONFIG;
63
+ if (env && fs.existsSync(env))
64
+ return { configPath: path.resolve(env) };
65
+ // process.argv[1] over require.main.filename: the latter realpath-resolves
66
+ // symlinks, which under `npm link` / workspaces / a local install jumps the
67
+ // entry *out* of the project (to the linked source) and breaks the walk.
68
+ // process.argv[1] preserves the path the binary was actually invoked at —
69
+ // inside the project's node_modules — which is what we want to walk up from.
70
+ const entry = process.argv[1] ?? require.main?.filename;
71
+ const entryDir = entry ? path.dirname(entry) : undefined;
72
+ for (const dir of searchStarts(start, entryDir, process.cwd())) {
73
+ const found = walkUp(dir);
74
+ if (found)
75
+ return { configPath: found };
76
+ }
77
+ throw new Error("No runspec configuration found.\nExpected runspec.toml inside your package directory.\n\nRun 'runspec init' to create one.");
78
+ }
79
+ /**
80
+ * Ordered, de-duplicated list of directories to walk up from, mirroring the
81
+ * resolution order documented on `findConfig`. Pure (no I/O) so the priority
82
+ * logic is unit-testable without a filesystem fixture.
83
+ */
84
+ function searchStarts(start, entryDir, cwd) {
85
+ const out = [];
86
+ if (start)
87
+ out.push(path.resolve(start));
88
+ if (entryDir)
89
+ out.push(path.resolve(entryDir));
90
+ out.push(path.resolve(cwd));
91
+ return [...new Set(out)];
92
+ }
93
+ /**
94
+ * Walk up from `start` looking for a `runspec.toml`, skipping any found under a
95
+ * `node_modules` path (a dependency's spec, not the project's). Returns the
96
+ * path or null. Exported for testing.
97
+ */
98
+ function walkUp(start) {
99
+ let dir = path.resolve(start);
41
100
  while (true) {
42
101
  const runspecToml = path.join(dir, 'runspec.toml');
43
- if (fs.existsSync(runspecToml)) {
44
- return { configPath: runspecToml };
102
+ if (fs.existsSync(runspecToml) && !isInNodeModules(dir)) {
103
+ return runspecToml;
45
104
  }
46
105
  const parent = path.dirname(dir);
47
106
  if (parent === dir)
48
- break;
107
+ return null;
49
108
  dir = parent;
50
109
  }
51
- throw new Error("No runspec configuration found.\nExpected runspec.toml inside your package directory.\n\nRun 'runspec init' to create one.");
110
+ }
111
+ function isInNodeModules(dir) {
112
+ return dir.split(path.sep).includes('node_modules');
52
113
  }
53
114
  //# sourceMappingURL=finder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"finder.js","sourceRoot":"","sources":["../src/finder.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,gCAiBC;AApBD,uCAAyB;AACzB,2CAA6B;AAE7B,SAAgB,UAAU,CAAC,KAAc;IACvC,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAE/C,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QACnD,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC/B,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;QACrC,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IAED,MAAM,IAAI,KAAK,CACb,4HAA4H,CAC7H,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"finder.js","sourceRoot":"","sources":["../src/finder.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,gCAoBC;AAOD,oCAMC;AAOD,wBAWC;AA1ED,uCAAyB;AACzB,2CAA6B;AAE7B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,UAAU,CAAC,KAAc;IACvC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACvC,IAAI,GAAG,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAExE,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,0EAA0E;IAC1E,6EAA6E;IAC7E,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAEzD,KAAK,MAAM,GAAG,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;QAC/D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,KAAK;YAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAC1C,CAAC;IAED,MAAM,IAAI,KAAK,CACb,4HAA4H,CAC7H,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,KAAyB,EAAE,QAA4B,EAAE,GAAW;IAC/F,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,KAAK;QAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,IAAI,QAAQ;QAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC/C,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,SAAgB,MAAM,CAAC,KAAa;IAClC,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QACnD,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;YACxD,OAAO,WAAW,CAAC;QACrB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAChC,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,GAAW;IAClC,OAAO,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;AACtD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runspec-node",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Node/TypeScript language pack for runspec",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/finder.ts CHANGED
@@ -1,21 +1,79 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
 
4
+ /**
5
+ * Locate the governing `runspec.toml`.
6
+ *
7
+ * Resolution order mirrors the Python `parse()`:
8
+ * 1. `RUNSPEC_CONFIG` env var — explicit override.
9
+ * 2. an explicit `start` directory, when one is passed.
10
+ * 3. **caller-relative** — walk up from the entry script's directory
11
+ * (`require.main` / `process.argv[1]`). This is what lets an installed
12
+ * runnable find its own config from *any* working directory — e.g. when a
13
+ * controller launches `bin/greet` over SSH with `cwd` = the login home,
14
+ * not the package. Mirrors Python resolving relative to the installed
15
+ * package, "what makes installed entry points work from any working
16
+ * directory".
17
+ * 4. **cwd-relative** — walk up from `process.cwd()` (the historical
18
+ * behaviour, kept as the final fallback).
19
+ *
20
+ * A `runspec.toml` found *inside* a `node_modules` path is ignored: those
21
+ * belong to installed dependencies (including runspec-node's own bundled CLI
22
+ * spec), never the project being discovered.
23
+ */
4
24
  export function findConfig(start?: string): { configPath: string } {
5
- let dir = path.resolve(start ?? process.cwd());
25
+ const env = process.env.RUNSPEC_CONFIG;
26
+ if (env && fs.existsSync(env)) return { configPath: path.resolve(env) };
6
27
 
28
+ // process.argv[1] over require.main.filename: the latter realpath-resolves
29
+ // symlinks, which under `npm link` / workspaces / a local install jumps the
30
+ // entry *out* of the project (to the linked source) and breaks the walk.
31
+ // process.argv[1] preserves the path the binary was actually invoked at —
32
+ // inside the project's node_modules — which is what we want to walk up from.
33
+ const entry = process.argv[1] ?? require.main?.filename;
34
+ const entryDir = entry ? path.dirname(entry) : undefined;
35
+
36
+ for (const dir of searchStarts(start, entryDir, process.cwd())) {
37
+ const found = walkUp(dir);
38
+ if (found) return { configPath: found };
39
+ }
40
+
41
+ throw new Error(
42
+ "No runspec configuration found.\nExpected runspec.toml inside your package directory.\n\nRun 'runspec init' to create one.",
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Ordered, de-duplicated list of directories to walk up from, mirroring the
48
+ * resolution order documented on `findConfig`. Pure (no I/O) so the priority
49
+ * logic is unit-testable without a filesystem fixture.
50
+ */
51
+ export function searchStarts(start: string | undefined, entryDir: string | undefined, cwd: string): string[] {
52
+ const out: string[] = [];
53
+ if (start) out.push(path.resolve(start));
54
+ if (entryDir) out.push(path.resolve(entryDir));
55
+ out.push(path.resolve(cwd));
56
+ return [...new Set(out)];
57
+ }
58
+
59
+ /**
60
+ * Walk up from `start` looking for a `runspec.toml`, skipping any found under a
61
+ * `node_modules` path (a dependency's spec, not the project's). Returns the
62
+ * path or null. Exported for testing.
63
+ */
64
+ export function walkUp(start: string): string | null {
65
+ let dir = path.resolve(start);
7
66
  while (true) {
8
67
  const runspecToml = path.join(dir, 'runspec.toml');
9
- if (fs.existsSync(runspecToml)) {
10
- return { configPath: runspecToml };
68
+ if (fs.existsSync(runspecToml) && !isInNodeModules(dir)) {
69
+ return runspecToml;
11
70
  }
12
-
13
71
  const parent = path.dirname(dir);
14
- if (parent === dir) break;
72
+ if (parent === dir) return null;
15
73
  dir = parent;
16
74
  }
75
+ }
17
76
 
18
- throw new Error(
19
- "No runspec configuration found.\nExpected runspec.toml inside your package directory.\n\nRun 'runspec init' to create one.",
20
- );
77
+ function isInNodeModules(dir: string): boolean {
78
+ return dir.split(path.sep).includes('node_modules');
21
79
  }
@@ -0,0 +1,94 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { findConfig, searchStarts, walkUp } from '../src/finder';
5
+
6
+ function tmpdir(): string {
7
+ return fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'runspec-finder-'));
8
+ }
9
+
10
+ function writeToml(dir: string): string {
11
+ fs.mkdirSync(dir, { recursive: true });
12
+ const p = path.join(dir, 'runspec.toml');
13
+ fs.writeFileSync(p, '[greet]\ndescription = "x"\n');
14
+ return p;
15
+ }
16
+
17
+ describe('searchStarts — resolution priority', () => {
18
+ it('orders explicit start, then entry dir, then cwd', () => {
19
+ expect(searchStarts('/a', '/b', '/c')).toEqual([path.resolve('/a'), path.resolve('/b'), path.resolve('/c')]);
20
+ });
21
+
22
+ it('omits a missing explicit start / entry dir', () => {
23
+ expect(searchStarts(undefined, undefined, '/c')).toEqual([path.resolve('/c')]);
24
+ expect(searchStarts(undefined, '/b', '/c')).toEqual([path.resolve('/b'), path.resolve('/c')]);
25
+ });
26
+
27
+ it('de-duplicates while preserving order (cwd === entry dir)', () => {
28
+ expect(searchStarts(undefined, '/same', '/same')).toEqual([path.resolve('/same')]);
29
+ });
30
+ });
31
+
32
+ describe('walkUp', () => {
33
+ let dir: string;
34
+ afterEach(() => {
35
+ if (dir) fs.rmSync(dir, { recursive: true, force: true });
36
+ });
37
+
38
+ it('finds runspec.toml walking up from a nested dir', () => {
39
+ dir = tmpdir();
40
+ const toml = writeToml(dir);
41
+ const nested = path.join(dir, 'a', 'b', 'c');
42
+ fs.mkdirSync(nested, { recursive: true });
43
+ expect(walkUp(nested)).toBe(toml);
44
+ });
45
+
46
+ it('ignores a runspec.toml that lives under node_modules', () => {
47
+ dir = tmpdir();
48
+ const projectToml = writeToml(dir); // {root}/runspec.toml — the project's
49
+ // A dependency ships its own runspec.toml (e.g. runspec-node's CLI spec).
50
+ const depDir = path.join(dir, 'node_modules', 'runspec-node', 'dist');
51
+ writeToml(depDir);
52
+ // Walking up from inside node_modules must skip the dep's toml and resolve
53
+ // the project's — this is the installed-CLI discovery case.
54
+ expect(walkUp(depDir)).toBe(projectToml);
55
+ });
56
+
57
+ it('returns null when no runspec.toml exists up the tree', () => {
58
+ dir = tmpdir();
59
+ const nested = path.join(dir, 'x', 'y');
60
+ fs.mkdirSync(nested, { recursive: true });
61
+ expect(walkUp(nested)).toBeNull();
62
+ });
63
+ });
64
+
65
+ describe('findConfig', () => {
66
+ let dir: string;
67
+ const savedEnv = process.env.RUNSPEC_CONFIG;
68
+ afterEach(() => {
69
+ if (savedEnv === undefined) delete process.env.RUNSPEC_CONFIG;
70
+ else process.env.RUNSPEC_CONFIG = savedEnv;
71
+ if (dir) fs.rmSync(dir, { recursive: true, force: true });
72
+ });
73
+
74
+ it('honours RUNSPEC_CONFIG when set', () => {
75
+ dir = tmpdir();
76
+ const toml = writeToml(path.join(dir, 'custom'));
77
+ process.env.RUNSPEC_CONFIG = toml;
78
+ // cwd has no toml, but the env var wins regardless.
79
+ expect(findConfig(dir).configPath).toBe(path.resolve(toml));
80
+ });
81
+
82
+ it('resolves from an explicit start dir', () => {
83
+ delete process.env.RUNSPEC_CONFIG;
84
+ dir = tmpdir();
85
+ const toml = writeToml(dir);
86
+ expect(findConfig(path.join(dir, 'sub', 'dir')).configPath).toBe(toml);
87
+ });
88
+
89
+ it('throws a helpful error when nothing is found', () => {
90
+ delete process.env.RUNSPEC_CONFIG;
91
+ dir = tmpdir(); // empty, no runspec.toml anywhere up the chain inside the tmp root
92
+ expect(() => findConfig(dir)).toThrow(/No runspec configuration found/);
93
+ });
94
+ });