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