@farmslot/recipe-cli 0.11.4 → 0.12.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/CHANGELOG.md +6 -0
- package/dist/discovery-index.d.ts +9 -0
- package/dist/discovery-index.d.ts.map +1 -1
- package/dist/discovery-index.js +4 -1
- package/dist/discovery-index.js.map +1 -1
- package/dist/harness/adapter-plugins.d.ts +2 -1
- package/dist/harness/adapter-plugins.d.ts.map +1 -1
- package/dist/harness/adapter-plugins.js +1 -1
- package/dist/harness/adapter-plugins.js.map +1 -1
- package/dist/harness/adapters.d.ts +1 -0
- package/dist/harness/adapters.d.ts.map +1 -1
- package/dist/harness/adapters.js +1 -1
- package/dist/harness/adapters.js.map +1 -1
- package/dist/harness/catalog.d.ts +8 -1
- package/dist/harness/catalog.d.ts.map +1 -1
- package/dist/harness/catalog.js +125 -1
- package/dist/harness/catalog.js.map +1 -1
- package/dist/harness/commands/run.js +52 -8
- package/dist/harness/commands/run.js.map +1 -1
- package/dist/harness/conformance.d.ts +35 -0
- package/dist/harness/conformance.d.ts.map +1 -0
- package/dist/harness/conformance.js +247 -0
- package/dist/harness/conformance.js.map +1 -0
- package/dist/harness/context-state.d.ts +55 -0
- package/dist/harness/context-state.d.ts.map +1 -1
- package/dist/harness/context-state.js +11 -0
- package/dist/harness/context-state.js.map +1 -1
- package/dist/harness/context.d.ts +45 -2
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js +447 -26
- package/dist/harness/context.js.map +1 -1
- package/dist/harness/execution-provenance.d.ts +9 -5
- package/dist/harness/execution-provenance.d.ts.map +1 -1
- package/dist/harness/execution-provenance.js +56 -5
- package/dist/harness/execution-provenance.js.map +1 -1
- package/dist/harness/harness-cli.d.ts +3 -0
- package/dist/harness/harness-cli.d.ts.map +1 -1
- package/dist/harness/harness-cli.js +9 -3
- package/dist/harness/harness-cli.js.map +1 -1
- package/dist/harness/index.d.ts +9 -7
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +9 -7
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/library-provenance.d.ts +2 -1
- package/dist/harness/library-provenance.d.ts.map +1 -1
- package/dist/harness/library-provenance.js +5 -1
- package/dist/harness/library-provenance.js.map +1 -1
- package/dist/harness/paths.d.ts +4 -0
- package/dist/harness/paths.d.ts.map +1 -1
- package/dist/harness/paths.js +22 -0
- package/dist/harness/paths.js.map +1 -1
- package/dist/harness/project-host.d.ts +17 -0
- package/dist/harness/project-host.d.ts.map +1 -0
- package/dist/harness/project-host.js +92 -0
- package/dist/harness/project-host.js.map +1 -0
- package/dist/harness/recipe-library.d.ts +1 -1
- package/dist/harness/recipe-library.d.ts.map +1 -1
- package/dist/harness/recipe-library.js +16 -6
- package/dist/harness/recipe-library.js.map +1 -1
- package/dist/harness/recipe-validation.d.ts +1 -1
- package/dist/harness/recipe-validation.d.ts.map +1 -1
- package/dist/harness/recipe-validation.js +3 -3
- package/dist/harness/recipe-validation.js.map +1 -1
- package/dist/harness/run-diagnostics.d.ts +2 -0
- package/dist/harness/run-diagnostics.d.ts.map +1 -1
- package/dist/harness/run-diagnostics.js +41 -0
- package/dist/harness/run-diagnostics.js.map +1 -1
- package/dist/harness/run-engine.d.ts +19 -2
- package/dist/harness/run-engine.d.ts.map +1 -1
- package/dist/harness/run-engine.js +220 -17
- package/dist/harness/run-engine.js.map +1 -1
- package/dist/harness/run-options.d.ts +2 -0
- package/dist/harness/run-options.d.ts.map +1 -1
- package/dist/harness/run-options.js +7 -5
- package/dist/harness/run-options.js.map +1 -1
- package/package.json +5 -5
package/dist/harness/context.js
CHANGED
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
// and funding flags) are never part of the context: they stay explicit.
|
|
9
9
|
import { execFileSync } from 'node:child_process';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
11
12
|
import os from 'node:os';
|
|
12
13
|
import path from 'node:path';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import { portableRepoIdentity } from '@farmslot/agent-runtime';
|
|
16
|
+
import { DEFAULT_TASK_DIR, validateProjectRecipeConfig, } from '@farmslot/protocol';
|
|
17
|
+
import { findSlotByRepo, resolveSlotPoolDir, SlotByRepoError, } from '@farmslot/protocol/node/slot-by-repo';
|
|
18
|
+
import { resolveRecipeLibrarySources } from '@farmslot/recipe-runner';
|
|
19
|
+
import { adapterPlugin, declaredAdapters, libraryName, } from './adapter-plugins.js';
|
|
20
|
+
import { adapterForPlatform, adapterPortEnv, checkoutRemote, detectAdapterMatch, harnessAdapters, isPlatformTarget, } from './adapters.js';
|
|
16
21
|
import { optionValues } from './command-contract.js';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
22
|
+
import { ProjectBindingError, } from './context-state.js';
|
|
23
|
+
import { inputSourceSnapshot, providerSourceSnapshot } from './execution-provenance.js';
|
|
24
|
+
import { configureHarnessHost, validateRelativeRecipePath, } from './host.js';
|
|
25
|
+
import { gitLibraryProvenance } from './library-provenance.js';
|
|
26
|
+
import { DEFAULT_RECIPE_RUNTIME_DIR, isPathWithin as within, recipeOutputRoots, recipeRuntimeDir, } from './paths.js';
|
|
19
27
|
/**
|
|
20
28
|
* Resolve the invocation's context. Throws AdapterAmbiguousError when detection
|
|
21
29
|
* is reached and more than one adapter matches the target.
|
|
@@ -38,13 +46,15 @@ export async function resolveHarnessContext(options) {
|
|
|
38
46
|
// slot and detection read the Git top level.
|
|
39
47
|
const root = targetFlag ? target.value : (gitTopLevel(target.value) ?? target.value);
|
|
40
48
|
const runtimeDir = optionValues(options.tokens, '--runtime-dir').at(-1);
|
|
41
|
-
const binding = readBinding(root, runtimeDir);
|
|
49
|
+
const binding = readBinding(options.runtimeTarget ?? root, runtimeDir);
|
|
42
50
|
const runtime = binding.runtime;
|
|
43
51
|
const pool = options.slotPoolDir
|
|
44
52
|
? { dir: options.slotPoolDir, source: 'option' }
|
|
45
53
|
: resolveSlotPoolDir();
|
|
46
54
|
const pooled = pool
|
|
47
|
-
? await poolSlot(root, pool.dir, pool.source === 'farmslot-node' ? 'slot-config (~/farmslot-node/pool)' : 'slot-config'
|
|
55
|
+
? await poolSlot(root, pool.dir, pool.source === 'farmslot-node' ? 'slot-config (~/farmslot-node/pool)' : 'slot-config', options.slotSelection === 'identity'
|
|
56
|
+
? undefined
|
|
57
|
+
: optionValues(options.tokens, '--slot').at(-1), options.strictSlot ?? true, options.slotSelection)
|
|
48
58
|
: 'no-pool-dir';
|
|
49
59
|
const pooledSlot = typeof pooled === 'object' ? pooled : undefined;
|
|
50
60
|
// The pool's slot keeps the owned runtime context's ports beside its own
|
|
@@ -58,7 +68,12 @@ export async function resolveHarnessContext(options) {
|
|
|
58
68
|
? // An explicit --runtime-dir scratch runtime names its own slot:
|
|
59
69
|
// identity and ports describe that runtime, the pool's ports filling
|
|
60
70
|
// only what it lacks. The default context keeps the pool's identity.
|
|
61
|
-
{
|
|
71
|
+
{
|
|
72
|
+
...owned,
|
|
73
|
+
poolFile: pooledSlot.slot.poolFile,
|
|
74
|
+
poolSlot: pooledSlot.slot.value,
|
|
75
|
+
ports: { ...pooledSlot.slot.ports, ...owned.ports },
|
|
76
|
+
}
|
|
62
77
|
: {
|
|
63
78
|
...pooledSlot.slot,
|
|
64
79
|
ports: { ...pooledSlot.slot.ports, ...runtimePorts(runtime) },
|
|
@@ -83,6 +98,7 @@ export async function resolveHarnessContext(options) {
|
|
|
83
98
|
target,
|
|
84
99
|
...(slot ? { slot } : {}),
|
|
85
100
|
...(binding.ignored ? { ignoredBinding: binding.ignored } : {}),
|
|
101
|
+
...(binding.path ? { runtimeConfigPath: binding.path } : {}),
|
|
86
102
|
};
|
|
87
103
|
}
|
|
88
104
|
// Each generic port option, and the slot ports that fill it, in order.
|
|
@@ -293,12 +309,12 @@ function readBinding(root, runtimeDir) {
|
|
|
293
309
|
const runtime = value;
|
|
294
310
|
const repoRoot = typeof runtime.repoRoot === 'string' ? runtime.repoRoot : undefined;
|
|
295
311
|
return runtimeContextOwned(root, file, runtime)
|
|
296
|
-
? { runtime }
|
|
312
|
+
? { runtime, path: file }
|
|
297
313
|
: { ignored: { path: file, repoRoot: repoRoot ?? null } };
|
|
298
314
|
}
|
|
299
315
|
/**
|
|
300
316
|
* Whether the runtime context read from `file` belongs to the checkout `root`:
|
|
301
|
-
* its `repoRoot` is
|
|
317
|
+
* its `repoRoot` is the selected target or its Git top level (real paths), or, without
|
|
302
318
|
* `repoRoot`, the file sits inside `root`. Anything else is another
|
|
303
319
|
* checkout's (an inherited RECIPE_RUNTIME_CONTEXT) and describes nothing here.
|
|
304
320
|
*/
|
|
@@ -306,7 +322,7 @@ export function runtimeContextOwned(root, file, context) {
|
|
|
306
322
|
// The checkout itself, not one nested in it: a clone under <checkout>/temp
|
|
307
323
|
// has its own top level and inherits nothing.
|
|
308
324
|
return typeof context.repoRoot === 'string'
|
|
309
|
-
? samePath(context.repoRoot, gitTopLevel(root) ?? root)
|
|
325
|
+
? samePath(context.repoRoot, root) || samePath(context.repoRoot, gitTopLevel(root) ?? root)
|
|
310
326
|
: within(root, file);
|
|
311
327
|
}
|
|
312
328
|
function samePath(left, right) {
|
|
@@ -323,19 +339,6 @@ function homeRelative(dir) {
|
|
|
323
339
|
const home = os.homedir();
|
|
324
340
|
return dir === home || dir.startsWith(`${home}${path.sep}`) ? `~${dir.slice(home.length)}` : dir;
|
|
325
341
|
}
|
|
326
|
-
// Whether `inner` is `outer` or inside it, comparing real paths.
|
|
327
|
-
function within(outer, inner) {
|
|
328
|
-
try {
|
|
329
|
-
const relative = path.relative(fs.realpathSync(outer), fs.realpathSync(inner));
|
|
330
|
-
return !(relative === '..' ||
|
|
331
|
-
relative.startsWith(`..${path.sep}`) ||
|
|
332
|
-
path.isAbsolute(relative));
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
// A path that does not exist owns nothing.
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
342
|
const RUNTIME_PORTS = ['watcherPort', 'devServerPort', 'metroPort', 'cdpPort'];
|
|
340
343
|
function runtimeSlot(runtime) {
|
|
341
344
|
if (typeof runtime?.slotId !== 'string' || !runtime.slotId)
|
|
@@ -358,7 +361,7 @@ function runtimePorts(runtime) {
|
|
|
358
361
|
}
|
|
359
362
|
// The slot slot-config maps this checkout to, with its own platform (never the
|
|
360
363
|
// pool's) for the adapter step; 'no-pool-dir' when the directory does not read.
|
|
361
|
-
async function poolSlot(target, poolDir, detail) {
|
|
364
|
+
async function poolSlot(target, poolDir, detail, slotId, strict = true, selection) {
|
|
362
365
|
let real;
|
|
363
366
|
try {
|
|
364
367
|
real = fs.realpathSync(target);
|
|
@@ -371,9 +374,13 @@ async function poolSlot(target, poolDir, detail) {
|
|
|
371
374
|
}
|
|
372
375
|
let match;
|
|
373
376
|
try {
|
|
374
|
-
match = await findSlotByRepo(poolDir, real);
|
|
377
|
+
match = await findSlotByRepo(poolDir, real, { strict, ...(slotId ? { slotId } : {}) });
|
|
375
378
|
}
|
|
376
379
|
catch (error) {
|
|
380
|
+
if (error instanceof SlotByRepoError && selection === 'identity')
|
|
381
|
+
throw Object.assign(error, {
|
|
382
|
+
userAction: 'map this checkout to a single pool slot; use --target for a separate checkout',
|
|
383
|
+
});
|
|
377
384
|
// A pool directory that does not exist is none; one that does not read
|
|
378
385
|
// (EACCES and the like) is a real failure.
|
|
379
386
|
if (missing(error))
|
|
@@ -406,4 +413,418 @@ function missing(error) {
|
|
|
406
413
|
const code = error?.code;
|
|
407
414
|
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
408
415
|
}
|
|
416
|
+
// Authority and the checked source identity cannot be supplied by discovered JSON
|
|
417
|
+
// or changed by a caller editing the report data between resolution and loading.
|
|
418
|
+
const projectProviders = new WeakMap();
|
|
419
|
+
const projectLibraries = new WeakMap();
|
|
420
|
+
const providerOutputRoots = new WeakMap();
|
|
421
|
+
const importedProviderSources = new Map();
|
|
422
|
+
/** Read project metadata and resolve defaults without importing executable code. */
|
|
423
|
+
export async function resolveProjectContext(options) {
|
|
424
|
+
const cwd = options.cwd ?? process.cwd();
|
|
425
|
+
const requested = optionValues(options.tokens, '--target').at(-1);
|
|
426
|
+
const checkoutRoot = gitTopLevel(path.resolve(cwd, requested ?? '.')) ?? path.resolve(cwd, requested ?? '.');
|
|
427
|
+
if (!fs.existsSync(checkoutRoot)) {
|
|
428
|
+
throw new ProjectBindingError('TARGET_INCOMPATIBLE', `Checkout ${checkoutRoot} does not exist.`, 'select an existing checkout with --target');
|
|
429
|
+
}
|
|
430
|
+
const divider = options.tokens.indexOf('--');
|
|
431
|
+
const optionEnd = divider < 0 ? options.tokens.length : divider;
|
|
432
|
+
const contextOptions = {
|
|
433
|
+
...options,
|
|
434
|
+
tokens: [
|
|
435
|
+
...options.tokens.slice(0, optionEnd),
|
|
436
|
+
'--target',
|
|
437
|
+
checkoutRoot,
|
|
438
|
+
...options.tokens.slice(optionEnd),
|
|
439
|
+
],
|
|
440
|
+
adapter: false,
|
|
441
|
+
};
|
|
442
|
+
let context = await resolveHarnessContext(contextOptions);
|
|
443
|
+
let runtime = readBinding(checkoutRoot, optionValues(options.tokens, '--runtime-dir').at(-1)).runtime;
|
|
444
|
+
const registered = [...(options.projects ?? [])];
|
|
445
|
+
if (options.projectsDir) {
|
|
446
|
+
for (const name of fs.readdirSync(options.projectsDir).sort()) {
|
|
447
|
+
const root = path.join(options.projectsDir, name);
|
|
448
|
+
const file = path.join(root, 'project.json');
|
|
449
|
+
if (fs.existsSync(file))
|
|
450
|
+
registered.push({ root, config: readProject(file) });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const localFile = path.join(checkoutRoot, 'project.json');
|
|
454
|
+
const discovered = fs.existsSync(localFile)
|
|
455
|
+
? [{ root: checkoutRoot, config: readProject(localFile) }]
|
|
456
|
+
: [];
|
|
457
|
+
const entries = [
|
|
458
|
+
...registered,
|
|
459
|
+
...discovered.filter((entry) => !registered.some((existing) => existing.config.name === entry.config.name)),
|
|
460
|
+
];
|
|
461
|
+
const pool = context.slot?.value && context.slot.poolFile
|
|
462
|
+
? JSON.parse(fs.readFileSync(context.slot.poolFile, 'utf8'))
|
|
463
|
+
: undefined;
|
|
464
|
+
const poolSlot = context.slot?.value ? (context.slot.poolSlot ?? context.slot.value) : undefined;
|
|
465
|
+
const slot = pool?.slots?.find((entry) => entry.id === poolSlot);
|
|
466
|
+
const projectFlag = optionValues(options.tokens, '--project').at(-1);
|
|
467
|
+
const bindingName = stringField(runtime, 'project');
|
|
468
|
+
const slotName = slot?.project ?? pool?.project;
|
|
469
|
+
const named = projectFlag ?? bindingName ?? slotName;
|
|
470
|
+
const remote = checkoutRemote(checkoutRoot) || undefined;
|
|
471
|
+
let source = projectFlag
|
|
472
|
+
? 'flag'
|
|
473
|
+
: bindingName
|
|
474
|
+
? 'binding'
|
|
475
|
+
: slotName
|
|
476
|
+
? 'slot'
|
|
477
|
+
: 'detect';
|
|
478
|
+
let candidates = named
|
|
479
|
+
? entries.filter((entry) => entry.config.name === named)
|
|
480
|
+
: entries.filter((entry) => {
|
|
481
|
+
if (samePath(entry.root, checkoutRoot))
|
|
482
|
+
return true;
|
|
483
|
+
return (remote !== undefined &&
|
|
484
|
+
repositoryIdentity(remote) === repositoryIdentity(entry.config.repoUrl));
|
|
485
|
+
});
|
|
486
|
+
if (!named && candidates.length === 0 && options.defaultProject) {
|
|
487
|
+
candidates = entries.filter((entry) => entry.config.name === options.defaultProject);
|
|
488
|
+
source = 'default';
|
|
489
|
+
}
|
|
490
|
+
if (candidates.length !== 1) {
|
|
491
|
+
throw new ProjectBindingError(candidates.length > 1 ? 'PROJECT_AMBIGUOUS' : 'PROJECT_NOT_FOUND', candidates.length > 1
|
|
492
|
+
? 'More than one project matches this checkout.'
|
|
493
|
+
: `No project binding found${named ? ` for ${named}` : ''}.`, 'select a registered project with --project <name>', candidates.map((entry) => entry.config.name));
|
|
494
|
+
}
|
|
495
|
+
const selected = candidates[0];
|
|
496
|
+
const config = selected.config;
|
|
497
|
+
const root = fs.realpathSync(selected.root);
|
|
498
|
+
const recipe = config.recipe;
|
|
499
|
+
if (!recipe?.provider?.module) {
|
|
500
|
+
throw new ProjectBindingError('PROVIDER_MISSING', `Project ${config.name} has no recipe provider.`, 'configure recipe.provider in project.json');
|
|
501
|
+
}
|
|
502
|
+
const apps = config.apps ?? (recipe.app ? [recipe.app] : []);
|
|
503
|
+
const appFlag = optionValues(options.tokens, '--app').at(-1);
|
|
504
|
+
const selectedApp = appFlag ?? stringField(runtime, 'app') ?? slot?.app;
|
|
505
|
+
const appDirectory = requested ? path.resolve(cwd, requested) : cwd;
|
|
506
|
+
const detectedApps = apps.filter((entry) => within(path.join(checkoutRoot, entry), appDirectory));
|
|
507
|
+
if (!selectedApp && detectedApps.length > 1) {
|
|
508
|
+
throw new ProjectBindingError('APP_AMBIGUOUS', 'More than one declared app contains this directory.', 'select an app with --app <path>', detectedApps);
|
|
509
|
+
}
|
|
510
|
+
const app = selectedApp ?? detectedApps[0] ?? recipe.app ?? (apps.length === 1 ? apps[0] : undefined);
|
|
511
|
+
if ((app && !apps.includes(app)) || (!app && apps.length > 1)) {
|
|
512
|
+
throw new ProjectBindingError(app ? 'APP_INCOMPATIBLE' : 'APP_AMBIGUOUS', app
|
|
513
|
+
? `App ${app} is not declared by project ${config.name}.`
|
|
514
|
+
: `Project ${config.name} needs an app selection.`, 'select a declared app with --app <path>', apps);
|
|
515
|
+
}
|
|
516
|
+
const target = app
|
|
517
|
+
? path.join(checkoutRoot, validateRelativeRecipePath('--app', app))
|
|
518
|
+
: checkoutRoot;
|
|
519
|
+
if (!fs.existsSync(target) || !within(checkoutRoot, target)) {
|
|
520
|
+
throw new ProjectBindingError('TARGET_INCOMPATIBLE', `Target ${target} is outside or missing from this checkout.`, 'select an existing checkout and app');
|
|
521
|
+
}
|
|
522
|
+
if (target !== checkoutRoot) {
|
|
523
|
+
context = await resolveHarnessContext({ ...contextOptions, runtimeTarget: target });
|
|
524
|
+
runtime = readBinding(target, optionValues(options.tokens, '--runtime-dir').at(-1)).runtime;
|
|
525
|
+
const targetProject = stringField(runtime, 'project');
|
|
526
|
+
if (targetProject && targetProject !== config.name && !projectFlag) {
|
|
527
|
+
throw new ProjectBindingError('PROJECT_INCOMPATIBLE', `App runtime names project ${targetProject}, not ${config.name}.`, 'select the intended project explicitly with --project');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (remote &&
|
|
531
|
+
config.repoUrl &&
|
|
532
|
+
repositoryIdentity(remote) !== repositoryIdentity(config.repoUrl)) {
|
|
533
|
+
throw new ProjectBindingError('PROJECT_INCOMPATIBLE', `Project ${config.name} does not match checkout origin ${remote}.`, 'select the project registered for this checkout');
|
|
534
|
+
}
|
|
535
|
+
const configured = registered.includes(selected);
|
|
536
|
+
validateProjectRecipeConfig(recipe);
|
|
537
|
+
const runtimeDir = validateRelativeRecipePath('runtime directory', optionValues(options.tokens, '--runtime-dir').at(-1) ?? recipeRuntimeDir());
|
|
538
|
+
const artifactDir = validateRelativeRecipePath('artifact directory', optionValues(options.tokens, '--artifacts-dir').at(-1) ?? config.paths.artifactDir);
|
|
539
|
+
const outputPaths = {
|
|
540
|
+
checkoutRoot,
|
|
541
|
+
runtimeDir,
|
|
542
|
+
artifactDir,
|
|
543
|
+
farmRuntimeDir: validateRelativeRecipePath('farm runtime directory', config.paths.runtimeDir),
|
|
544
|
+
};
|
|
545
|
+
const excludedRoots = recipeOutputRoots(target, outputPaths);
|
|
546
|
+
const environment = { ...pool?.env, ...(options.load?.env ?? process.env) };
|
|
547
|
+
const existingSourceRoot = (value, label = value) => {
|
|
548
|
+
try {
|
|
549
|
+
return fs.realpathSync(value);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
if (!missing(error))
|
|
553
|
+
throw error;
|
|
554
|
+
throw new ProjectBindingError('SOURCE_ROOT_MISSING', `Configured source root ${label} does not exist.`, 'configure an existing provider or library path in the selected pool or project');
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
const sourceRoot = (ref) => {
|
|
558
|
+
if (!configured)
|
|
559
|
+
throw new ProjectBindingError('SOURCE_UNAUTHORIZED', 'Discovered root references cannot read operator environment or checkout paths.', 'register the project before resolving provider or library root references');
|
|
560
|
+
const value = ref.env ? environment[ref.env] : path.resolve(checkoutRoot, ref.projectPath);
|
|
561
|
+
if (!value)
|
|
562
|
+
throw new ProjectBindingError('SOURCE_ROOT_MISSING', `Missing configured source root ${ref.env ?? ref.projectPath}.`, `set ${ref.env ?? 'the project path'} in the operator environment or selected pool.env`);
|
|
563
|
+
return existingSourceRoot(value, ref.env ?? ref.projectPath);
|
|
564
|
+
};
|
|
565
|
+
const provider = resolveProvider(recipe.provider, root, configured, recipe.provider.root ? sourceRoot(recipe.provider.root) : undefined, excludedRoots);
|
|
566
|
+
const overrides = await resolveRecipeLibrarySources({
|
|
567
|
+
cliEntries: optionValues(options.tokens, '--library'),
|
|
568
|
+
...(options.load?.env ? { env: options.load.env } : {}),
|
|
569
|
+
});
|
|
570
|
+
const libraries = overrides.map((entry) => {
|
|
571
|
+
const libraryRoot = existingSourceRoot(path.resolve(cwd, entry.root));
|
|
572
|
+
const identity = inputSourceSnapshot(libraryRoot, excludedRoots);
|
|
573
|
+
const name = libraryName({ ...entry, root: libraryRoot });
|
|
574
|
+
return {
|
|
575
|
+
...entry,
|
|
576
|
+
root: libraryRoot,
|
|
577
|
+
name,
|
|
578
|
+
identity,
|
|
579
|
+
};
|
|
580
|
+
});
|
|
581
|
+
for (const entry of recipe.libraries ?? []) {
|
|
582
|
+
const override = libraries.find((library) => library.name === entry.name);
|
|
583
|
+
if (override) {
|
|
584
|
+
const original = typeof entry.source === 'string'
|
|
585
|
+
? path.resolve(root, entry.source)
|
|
586
|
+
: entry.source.env
|
|
587
|
+
? `env:${entry.source.env}`
|
|
588
|
+
: path.resolve(checkoutRoot, entry.source.projectPath);
|
|
589
|
+
override.overrides ??= original;
|
|
590
|
+
override.overriddenSource = {
|
|
591
|
+
root: original,
|
|
592
|
+
owner: entry.owner,
|
|
593
|
+
revision: entry.revision,
|
|
594
|
+
};
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const libraryRoot = typeof entry.source === 'string'
|
|
598
|
+
? existingSourceRoot(path.resolve(root, entry.source))
|
|
599
|
+
: sourceRoot(entry.source);
|
|
600
|
+
const identity = inputSourceSnapshot(libraryRoot, excludedRoots);
|
|
601
|
+
if (identity.head && !entry.revision) {
|
|
602
|
+
throw new ProjectBindingError('SOURCE_REVISION_MISSING', `Library ${entry.name} is a repository source without a pinned revision.`, 'pin the library revision in project.json');
|
|
603
|
+
}
|
|
604
|
+
checkRevision(entry.revision, identity.head, `library ${entry.name}`);
|
|
605
|
+
libraries.push({
|
|
606
|
+
name: entry.name,
|
|
607
|
+
root: libraryRoot,
|
|
608
|
+
owner: entry.owner,
|
|
609
|
+
revision: entry.revision,
|
|
610
|
+
identity,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
for (const library of libraries) {
|
|
614
|
+
library.provenance = {
|
|
615
|
+
kind: 'library',
|
|
616
|
+
trust: 'unknown',
|
|
617
|
+
name: library.name,
|
|
618
|
+
path: library.root,
|
|
619
|
+
...(await gitLibraryProvenance(library.root, library.identity)),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
context.target = {
|
|
623
|
+
value: target,
|
|
624
|
+
source: requested ? 'flag' : 'default',
|
|
625
|
+
detail: requested ? '--target' : 'cwd',
|
|
626
|
+
};
|
|
627
|
+
const platform = (value) => {
|
|
628
|
+
if (value === undefined)
|
|
629
|
+
return undefined;
|
|
630
|
+
const mapped = options.slotAdapter?.(value);
|
|
631
|
+
if (mapped)
|
|
632
|
+
return mapped;
|
|
633
|
+
const candidate = adapterForPlatform(value);
|
|
634
|
+
return candidate && (harnessAdapters().has(candidate) || candidate === recipe.adapter)
|
|
635
|
+
? candidate
|
|
636
|
+
: undefined;
|
|
637
|
+
};
|
|
638
|
+
const adapter = flagAdapter(options.tokens, options.positionals) ??
|
|
639
|
+
sourced(stringField(runtime, 'adapter') ?? platform(stringField(runtime, 'platform')), 'binding', 'runtime-context') ??
|
|
640
|
+
sourced(platform(slot?.platform), 'slot', 'slot-config') ??
|
|
641
|
+
(harnessAdapters().list().length ? detectedAdapter(target, []) : undefined) ??
|
|
642
|
+
sourced(recipe.adapter ?? options.defaultAdapter, 'default', 'project.json');
|
|
643
|
+
if (adapter)
|
|
644
|
+
context.adapter = adapter;
|
|
645
|
+
context.project = {
|
|
646
|
+
name: config.name,
|
|
647
|
+
source,
|
|
648
|
+
root,
|
|
649
|
+
configPath: path.join(root, 'project.json'),
|
|
650
|
+
...outputPaths,
|
|
651
|
+
...(app ? { app } : {}),
|
|
652
|
+
domain: recipe.domain,
|
|
653
|
+
template: recipe.template,
|
|
654
|
+
...(recipe.manifest ? { manifest: path.resolve(root, recipe.manifest) } : {}),
|
|
655
|
+
provider,
|
|
656
|
+
libraries,
|
|
657
|
+
};
|
|
658
|
+
projectProviders.set(context, structuredClone(provider));
|
|
659
|
+
projectLibraries.set(context, structuredClone(libraries));
|
|
660
|
+
providerOutputRoots.set(context, excludedRoots);
|
|
661
|
+
return context;
|
|
662
|
+
}
|
|
663
|
+
/** Executable library authority comes from resolution, never mutable report metadata. */
|
|
664
|
+
export function authorizedProjectLibraries(context) {
|
|
665
|
+
const provider = projectProviders.get(context);
|
|
666
|
+
return structuredClone((projectLibraries.get(context) ?? []).filter((library) => provider?.authority === 'configured' || library.origin !== undefined));
|
|
667
|
+
}
|
|
668
|
+
/** The shared direct/hosted loader. Source identity is checked before import. */
|
|
669
|
+
export async function loadProjectProvider(context, options = {}) {
|
|
670
|
+
const provider = projectProviders.get(context);
|
|
671
|
+
if (!provider || !context.project) {
|
|
672
|
+
throw new ProjectBindingError('PROVIDER_MISSING', 'No resolved project provider is available.', 'resolve a project binding before loading its provider');
|
|
673
|
+
}
|
|
674
|
+
if (provider.authority === 'discovered' &&
|
|
675
|
+
!options.authorizedProviders?.some((entry) => entry === provider.ref || entry === provider.module)) {
|
|
676
|
+
throw new ProjectBindingError('PROVIDER_UNAUTHORIZED', `Provider ${provider.ref} was discovered but is not authorized.`, 'install/configure the provider or explicitly authorize its source');
|
|
677
|
+
}
|
|
678
|
+
const current = providerSourceSnapshot(provider.root, provider.module, providerOutputRoots.get(context));
|
|
679
|
+
if (current.head !== provider.identity.head ||
|
|
680
|
+
current.sourceFingerprint !== provider.identity.sourceFingerprint) {
|
|
681
|
+
throw new ProjectBindingError('PROVIDER_SOURCE_CHANGED', `Provider ${provider.ref} changed after resolution.`, 'resolve the current provider again');
|
|
682
|
+
}
|
|
683
|
+
const importedIdentity = importedProviderSources.get(provider.module);
|
|
684
|
+
if (importedIdentity !== undefined && importedIdentity !== current.sourceFingerprint) {
|
|
685
|
+
// ESM dependencies are cached for this process. Refuse to label cached code
|
|
686
|
+
// with a new source identity after another invocation changes the provider.
|
|
687
|
+
throw new ProjectBindingError('PROVIDER_SOURCE_CHANGED', `Provider ${provider.ref} changed after it was imported.`, 'restart the host before loading the changed provider');
|
|
688
|
+
}
|
|
689
|
+
const module = (await import(pathToFileURL(provider.module).href));
|
|
690
|
+
importedProviderSources.set(provider.module, current.sourceFingerprint);
|
|
691
|
+
const createProvider = module[provider.export];
|
|
692
|
+
if (typeof createProvider !== 'function') {
|
|
693
|
+
throw new ProjectBindingError('PROVIDER_INVALID', `Provider ${provider.ref} has no ${provider.export} factory.`, 'export createProvider(context) from the configured module');
|
|
694
|
+
}
|
|
695
|
+
const providerHost = module.providerHost;
|
|
696
|
+
const host = providerHost || options.host
|
|
697
|
+
? {
|
|
698
|
+
...providerHost,
|
|
699
|
+
...options.host,
|
|
700
|
+
name: options.host?.name ?? 'farmslot recipe',
|
|
701
|
+
}
|
|
702
|
+
: undefined;
|
|
703
|
+
if (host)
|
|
704
|
+
configureHarnessHost(host);
|
|
705
|
+
const result = (await createProvider({
|
|
706
|
+
...context,
|
|
707
|
+
...(host ? { host } : {}),
|
|
708
|
+
command: options.command,
|
|
709
|
+
options: options.options,
|
|
710
|
+
libraries: authorizedProjectLibraries(context),
|
|
711
|
+
}));
|
|
712
|
+
if (!result?.runtime || typeof result.runtime.id !== 'string') {
|
|
713
|
+
throw new ProjectBindingError('PROVIDER_INVALID', `Provider ${provider.ref} returned no runtime.`, 'return { runtime } from createProvider');
|
|
714
|
+
}
|
|
715
|
+
const selection = context.adapter?.value;
|
|
716
|
+
if (selection &&
|
|
717
|
+
selection !== result.runtime.id &&
|
|
718
|
+
!result.runtime.targets?.includes(selection)) {
|
|
719
|
+
throw new ProjectBindingError('TARGET_INCOMPATIBLE', `Provider runtime ${result.runtime.id} does not support ${selection}.`, 'select a target supported by the project provider');
|
|
720
|
+
}
|
|
721
|
+
const registry = harnessAdapters();
|
|
722
|
+
if (registry.has(result.runtime.id) && registry.get(result.runtime.id) !== result.runtime) {
|
|
723
|
+
throw new ProjectBindingError('PROVIDER_AMBIGUOUS', `Runtime ${result.runtime.id} is already registered by another provider.`, 'use one provider for each runtime id');
|
|
724
|
+
}
|
|
725
|
+
if (!registry.has(result.runtime.id))
|
|
726
|
+
registry.register(result.runtime);
|
|
727
|
+
context.adapter = {
|
|
728
|
+
value: result.runtime.id,
|
|
729
|
+
requested: selection ?? result.runtime.id,
|
|
730
|
+
source: context.adapter?.source ?? 'default',
|
|
731
|
+
detail: context.adapter?.detail ?? 'project provider',
|
|
732
|
+
};
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
function readProject(file) {
|
|
736
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
737
|
+
// project.json uses the existing pack spelling; registered protocol values
|
|
738
|
+
// already use camelCase. Only the binding's fields need normalization here.
|
|
739
|
+
return {
|
|
740
|
+
...raw,
|
|
741
|
+
repoUrl: raw.repoUrl ?? raw.repo_url ?? '',
|
|
742
|
+
defaultBranch: raw.defaultBranch ?? raw.default_branch ?? '',
|
|
743
|
+
paths: {
|
|
744
|
+
runtimeDir: raw.paths?.runtimeDir || raw.paths?.runtime_dir || '.agent',
|
|
745
|
+
artifactDir: raw.paths?.artifactDir || raw.paths?.artifact_dir || DEFAULT_TASK_DIR,
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function stringField(value, key) {
|
|
750
|
+
return typeof value?.[key] === 'string' ? value[key] : undefined;
|
|
751
|
+
}
|
|
752
|
+
function repositoryIdentity(value) {
|
|
753
|
+
const repository = portableRepoIdentity(value);
|
|
754
|
+
if (!repository)
|
|
755
|
+
return value;
|
|
756
|
+
const sshHost = (host) => {
|
|
757
|
+
const config = execFileSync('ssh', ['-G', '--', host], {
|
|
758
|
+
encoding: 'utf8',
|
|
759
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
760
|
+
timeout: 1000,
|
|
761
|
+
});
|
|
762
|
+
const resolved = /^hostname (.+)$/mu.exec(config)?.[1];
|
|
763
|
+
if (!resolved)
|
|
764
|
+
throw new ProjectBindingError('PROJECT_ORIGIN_INVALID', `Cannot resolve SSH repository host ${host}.`, 'check the operator SSH host configuration');
|
|
765
|
+
return resolved.toLowerCase();
|
|
766
|
+
};
|
|
767
|
+
if (URL.canParse(value)) {
|
|
768
|
+
const url = new URL(value);
|
|
769
|
+
if (['https:', 'http:', 'ssh:', 'git:'].includes(url.protocol))
|
|
770
|
+
return `${url.protocol === 'ssh:' ? sshHost(url.hostname) : url.hostname.toLowerCase()}${url.port ? `:${url.port}` : ''}/${repository}`;
|
|
771
|
+
}
|
|
772
|
+
const scp = /^(?:[^/@:]+@)?([^/:]+):(.+)$/u.exec(value);
|
|
773
|
+
return scp ? `${sshHost(scp[1])}/${repository}` : repository;
|
|
774
|
+
}
|
|
775
|
+
/** The package owning a resolved entry, including entries below nested source/dist directories. */
|
|
776
|
+
export function recipePackageRoot(file) {
|
|
777
|
+
let root = path.dirname(file);
|
|
778
|
+
while (!fs.existsSync(path.join(root, 'package.json'))) {
|
|
779
|
+
const parent = path.dirname(root);
|
|
780
|
+
if (parent === root)
|
|
781
|
+
throw new ProjectBindingError('PROVIDER_INVALID', 'Provider package metadata is missing.', 'install a valid provider package');
|
|
782
|
+
root = parent;
|
|
783
|
+
}
|
|
784
|
+
return root;
|
|
785
|
+
}
|
|
786
|
+
function resolveProvider(declaration, root, configured, declaredRoot, excludedRoots = []) {
|
|
787
|
+
let file;
|
|
788
|
+
try {
|
|
789
|
+
file = declaration.package
|
|
790
|
+
? createRequire(path.join(declaredRoot ?? root, 'package.json')).resolve(declaration.module)
|
|
791
|
+
: fs.realpathSync(path.resolve(declaredRoot ?? root, declaration.module));
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
if (!missing(error) && error.code !== 'MODULE_NOT_FOUND')
|
|
795
|
+
throw error;
|
|
796
|
+
throw new ProjectBindingError('PROVIDER_MISSING', `Provider ${declaration.module} is not available.`, 'install the configured provider or correct recipe.provider.module');
|
|
797
|
+
}
|
|
798
|
+
const packageRoot = declaration.package
|
|
799
|
+
? recipePackageRoot(file)
|
|
800
|
+
: (declaredRoot ?? (within(root, file) ? root : path.dirname(file)));
|
|
801
|
+
const metadata = declaration.package
|
|
802
|
+
? JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))
|
|
803
|
+
: undefined;
|
|
804
|
+
if (metadata && metadata.name !== declaration.package) {
|
|
805
|
+
throw new ProjectBindingError('PROVIDER_SOURCE_MISMATCH', `Module ${declaration.module} belongs to ${metadata.name}, not ${declaration.package}.`, 'use a module exported by the declared provider package');
|
|
806
|
+
}
|
|
807
|
+
const identity = providerSourceSnapshot(packageRoot, file, excludedRoots);
|
|
808
|
+
checkRevision(declaration.revision, identity.head, 'provider');
|
|
809
|
+
return {
|
|
810
|
+
ref: declaration.module,
|
|
811
|
+
module: fs.realpathSync(file),
|
|
812
|
+
export: declaration.export ?? 'createProvider',
|
|
813
|
+
package: declaration.package,
|
|
814
|
+
version: metadata?.version,
|
|
815
|
+
root: packageRoot,
|
|
816
|
+
revision: declaration.revision,
|
|
817
|
+
identity,
|
|
818
|
+
authority: configured
|
|
819
|
+
? 'configured'
|
|
820
|
+
: declaration.package && file.includes(`${path.sep}node_modules${path.sep}`)
|
|
821
|
+
? 'installed'
|
|
822
|
+
: 'discovered',
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function checkRevision(expected, actual, label) {
|
|
826
|
+
if (expected && expected !== actual) {
|
|
827
|
+
throw new ProjectBindingError('SOURCE_REVISION_MISMATCH', `Configured ${label} revision ${expected} differs from ${actual ?? 'an unversioned source'}.`, 'use the pinned source revision or update the operator configuration');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
409
830
|
//# sourceMappingURL=context.js.map
|