@ontrails/trails 1.0.0-beta.39 → 1.0.0-beta.41

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 (58) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +1 -1
  3. package/package.json +17 -17
  4. package/src/app.ts +4 -4
  5. package/src/cli.ts +1 -1
  6. package/src/completions.ts +1 -1
  7. package/src/mcp-app.ts +2 -2
  8. package/src/regrade/history.ts +148 -38
  9. package/src/regrade/plan-artifact.ts +31 -11
  10. package/src/release/native-bun-registry.ts +12 -1
  11. package/src/release/policy.ts +4 -1
  12. package/src/retired-topo-command.ts +1 -1
  13. package/src/run-watch.ts +1 -1
  14. package/src/run-wayfind-outline.ts +6 -2
  15. package/src/trails/adapter-check.ts +8 -8
  16. package/src/trails/add-surface.ts +2 -2
  17. package/src/trails/add-trail.ts +3 -3
  18. package/src/trails/add-verify.ts +2 -2
  19. package/src/trails/compile.ts +9 -9
  20. package/src/trails/completions-complete.ts +10 -10
  21. package/src/trails/completions.ts +2 -2
  22. package/src/trails/create-adapter.ts +185 -424
  23. package/src/trails/create-scaffold.ts +12 -12
  24. package/src/trails/create-versions.ts +8 -8
  25. package/src/trails/create.ts +35 -35
  26. package/src/trails/deprecate.ts +2 -2
  27. package/src/trails/dev-clean.ts +11 -11
  28. package/src/trails/dev-reset.ts +11 -11
  29. package/src/trails/dev-stats.ts +8 -8
  30. package/src/trails/dev-support.ts +1 -1
  31. package/src/trails/doctor.ts +4 -4
  32. package/src/trails/draft-promote.ts +3 -3
  33. package/src/trails/guide.ts +9 -9
  34. package/src/trails/load-app.ts +3 -3
  35. package/src/trails/regrade.ts +56 -27
  36. package/src/trails/release-check.ts +8 -8
  37. package/src/trails/release-smoke.ts +2 -2
  38. package/src/trails/revise.ts +2 -2
  39. package/src/trails/run-example.ts +9 -9
  40. package/src/trails/run-examples.ts +9 -9
  41. package/src/trails/run.ts +26 -26
  42. package/src/trails/survey.ts +45 -45
  43. package/src/trails/topo-activation.ts +2 -2
  44. package/src/trails/topo-history.ts +8 -8
  45. package/src/trails/topo-output-schemas.ts +5 -5
  46. package/src/trails/topo-pin.ts +6 -6
  47. package/src/trails/topo-read-support.ts +4 -3
  48. package/src/trails/topo-reports.ts +16 -16
  49. package/src/trails/topo-store-support.ts +18 -9
  50. package/src/trails/topo-support.ts +2 -2
  51. package/src/trails/topo-unpin.ts +12 -12
  52. package/src/trails/topo.ts +4 -4
  53. package/src/trails/validate.ts +2 -2
  54. package/src/trails/version-lifecycle-support.ts +17 -16
  55. package/src/trails/warden-guide.ts +8 -8
  56. package/src/trails/warden.ts +21 -21
  57. package/src/trails/wayfind-outline.ts +876 -0
  58. package/src/trails/wayfind.ts +74 -74
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  import {
6
+ adapterSourceExportKind,
7
+ adapterSourceExports,
6
8
  adapterTargetPlacements,
7
9
  checkAdapters,
8
10
  deriveAdapterTargetCatalog,
@@ -22,7 +24,7 @@ import {
22
24
  } from '@ontrails/core';
23
25
  import type { WorkspaceRootManifest } from '@ontrails/core';
24
26
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
25
- import { dirname, join, resolve } from 'node:path';
27
+ import { dirname, join, relative, resolve } from 'node:path';
26
28
  import { z } from 'zod';
27
29
 
28
30
  import {
@@ -47,7 +49,7 @@ const packageNameMessage =
47
49
 
48
50
  type CreateAdapterPlacement = AdapterTargetPlacement;
49
51
 
50
- const createAdapterPlacements = ['extracted'] as const;
52
+ const createAdapterPlacements = adapterTargetPlacements;
51
53
 
52
54
  interface CreateAdapterInput {
53
55
  readonly dryRun: boolean;
@@ -79,18 +81,7 @@ interface AdapterOperationPlan {
79
81
  interface WorkspacePackageManifest {
80
82
  readonly exports?: unknown;
81
83
  readonly name?: unknown;
82
- }
83
-
84
- interface LocalNamedReexport {
85
- readonly local: string;
86
- readonly specifier: string;
87
- readonly typeOnly: boolean;
88
- }
89
-
90
- interface LocalNamedImport {
91
- readonly imported: string;
92
- readonly specifier: string;
93
- readonly typeOnly: boolean;
84
+ readonly trails?: unknown;
94
85
  }
95
86
 
96
87
  const literal = (value: string): string => JSON.stringify(value);
@@ -169,6 +160,9 @@ const resolvePhysicalRootDir = (
169
160
  const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
170
161
  typeof value === 'object' && value !== null && !Array.isArray(value);
171
162
 
163
+ const toMutableRecord = (value: unknown): Record<string, unknown> =>
164
+ isRecord(value) ? { ...value } : {};
165
+
172
166
  const runtimeExportTarget = (value: unknown, depth = 0): string | undefined => {
173
167
  if (typeof value === 'string') {
174
168
  return value;
@@ -208,390 +202,13 @@ const packageRootExportTarget = (
208
202
  return resolve(target.packageRoot, exportTarget);
209
203
  };
210
204
 
211
- const maskSource = (source: string, options: { strings: boolean }): string => {
212
- const output = [...source];
213
- let index = 0;
214
-
215
- const maskRange = (start: number, end: number): void => {
216
- for (let cursor = start; cursor < end; cursor += 1) {
217
- if (output[cursor] !== '\n') {
218
- output[cursor] = ' ';
219
- }
220
- }
221
- };
222
-
223
- const skipQuoted = (quote: '"' | "'" | '`'): void => {
224
- const start = index;
225
- index += 1;
226
- while (index < source.length) {
227
- if (source[index] === '\\') {
228
- index += 2;
229
- continue;
230
- }
231
- if (source[index] === quote) {
232
- index += 1;
233
- break;
234
- }
235
- index += 1;
236
- }
237
- if (options.strings) {
238
- maskRange(start, index);
239
- }
240
- };
241
-
242
- while (index < source.length) {
243
- if (source.startsWith('//', index)) {
244
- const end = source.indexOf('\n', index + 2);
245
- const stop = end === -1 ? source.length : end;
246
- maskRange(index, stop);
247
- index = stop;
248
- continue;
249
- }
250
- if (source.startsWith('/*', index)) {
251
- const end = source.indexOf('*/', index + 2);
252
- const stop = end === -1 ? source.length : end + 2;
253
- maskRange(index, stop);
254
- index = stop;
255
- continue;
256
- }
257
- const char = source[index];
258
- if (char === '"' || char === "'" || char === '`') {
259
- skipQuoted(char);
260
- continue;
261
- }
262
- index += 1;
263
- }
264
-
265
- return output.join('');
266
- };
267
-
268
- const sameFileExportListLocalForIdentifier = (
269
- source: string,
270
- identifier: string,
271
- expected: 'type' | 'value'
272
- ): string | undefined => {
273
- const exportCode = maskSource(source, { strings: false });
274
- const stringsMaskedCode = maskSource(source, { strings: true });
275
- const exportListPattern =
276
- /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}(?<from>\s+from\s+['"][^'"]+['"])?/gu;
277
- for (const match of exportCode.matchAll(exportListPattern)) {
278
- if (!stringsMaskedCode.startsWith('export', match.index ?? 0)) {
279
- continue;
280
- }
281
-
282
- if (match.groups?.['from']) {
283
- continue;
284
- }
285
-
286
- const namedExports = match.groups?.['exports'] ?? '';
287
- for (const item of namedExports.split(',')) {
288
- const trimmedItem = item.trim();
289
- const specifierTypeOnly =
290
- Boolean(match.groups?.['typeOnly']) || trimmedItem.startsWith('type ');
291
- const specifierText = trimmedItem.replace(/^type\s+/u, '');
292
- const exported =
293
- /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
294
- specifierText
295
- )?.groups;
296
- if (
297
- !exported?.['local'] ||
298
- (exported['name'] ?? exported['local']) !== identifier
299
- ) {
300
- continue;
301
- }
302
- if (expected === 'type') {
303
- return exported['local'];
304
- }
305
- if (!specifierTypeOnly) {
306
- return exported['local'];
307
- }
308
- }
309
- }
310
-
311
- return undefined;
312
- };
313
-
314
- const declaresTypeBinding = (source: string, identifier: string): boolean => {
315
- const code = maskSource(source, { strings: true });
316
- const escapedIdentifier = identifier.replaceAll(
317
- /[.*+?^${}()|[\]\\]/gu,
318
- '\\$&'
319
- );
320
- return new RegExp(
321
- `(?:^|[;\\n\\r])\\s*(?:export\\s+)?(?:declare\\s+)?(?:interface|type)\\s+${escapedIdentifier}\\b`,
322
- 'u'
323
- ).test(code);
324
- };
325
-
326
- const declaresValueBinding = (source: string, identifier: string): boolean => {
327
- const code = maskSource(source, { strings: true });
328
- const escapedIdentifier = identifier.replaceAll(
329
- /[.*+?^${}()|[\]\\]/gu,
330
- '\\$&'
331
- );
332
- return new RegExp(
333
- `(?:^|[;\\n\\r])\\s*(?:export\\s+)?(?:(?:async\\s+)?function|const|let|var|class|enum)\\s+${escapedIdentifier}\\b`,
334
- 'u'
335
- ).test(code);
336
- };
337
-
338
- const sourceExportsIdentifier = (
339
- source: string,
340
- identifier: string,
341
- expected: 'type' | 'value'
342
- ): boolean => {
343
- const code = maskSource(source, { strings: true });
344
- const escapedIdentifier = identifier.replaceAll(
345
- /[.*+?^${}()|[\]\\]/gu,
346
- '\\$&'
347
- );
348
- const valueDeclarationPattern = new RegExp(
349
- `\\bexport\\s+(?:(?:async\\s+)?function|const|let|var|class|enum)\\s+${escapedIdentifier}\\b`,
350
- 'u'
351
- );
352
- if (expected === 'value' && valueDeclarationPattern.test(code)) {
353
- return true;
354
- }
355
-
356
- const typeDeclarationPattern = new RegExp(
357
- `\\bexport\\s+(?:declare\\s+)?(?:interface|type)\\s+${escapedIdentifier}\\b`,
358
- 'u'
359
- );
360
- const sameFileLocal = sameFileExportListLocalForIdentifier(
361
- source,
362
- identifier,
363
- expected
364
- );
365
- return (
366
- (expected === 'type' &&
367
- (typeDeclarationPattern.test(code) ||
368
- (sameFileLocal !== undefined &&
369
- declaresTypeBinding(source, sameFileLocal)))) ||
370
- (expected === 'value' &&
371
- sameFileLocal !== undefined &&
372
- declaresValueBinding(source, sameFileLocal))
373
- );
374
- };
375
-
376
- const localNamedImports = (
377
- source: string,
378
- identifier: string
379
- ): readonly LocalNamedImport[] => {
380
- const code = maskSource(source, { strings: false });
381
- const stringsMaskedCode = maskSource(source, { strings: true });
382
- const imports: LocalNamedImport[] = [];
383
- const pattern =
384
- /\bimport\s+(?<typeOnly>type\s+)?\{(?<imports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
385
-
386
- for (const match of code.matchAll(pattern)) {
387
- if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
388
- continue;
389
- }
390
-
391
- const specifier = match.groups?.['specifier'];
392
- if (!specifier?.startsWith('.')) {
393
- continue;
394
- }
395
-
396
- const namedImports = match.groups?.['imports'] ?? '';
397
- for (const item of namedImports.split(',')) {
398
- const trimmedItem = item.trim();
399
- if (!trimmedItem) {
400
- continue;
401
- }
402
-
403
- const specifierTypeOnly =
404
- Boolean(match.groups?.['typeOnly']) || trimmedItem.startsWith('type ');
405
- const specifierText = trimmedItem.replace(/^type\s+/u, '');
406
- const imported =
407
- /^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
408
- specifierText
409
- )?.groups;
410
- if (
411
- !imported?.['imported'] ||
412
- (imported['local'] ?? imported['imported']) !== identifier
413
- ) {
414
- continue;
415
- }
416
-
417
- imports.push({
418
- imported: imported['imported'],
419
- specifier,
420
- typeOnly: specifierTypeOnly,
421
- });
422
- }
423
- }
424
-
425
- return imports;
426
- };
427
-
428
- const localNamedReexports = (
429
- source: string,
430
- identifier: string
431
- ): readonly LocalNamedReexport[] => {
432
- const code = maskSource(source, { strings: false });
433
- const stringsMaskedCode = maskSource(source, { strings: true });
434
- const exports: LocalNamedReexport[] = [];
435
- const pattern =
436
- /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
437
-
438
- for (const match of code.matchAll(pattern)) {
439
- if (!stringsMaskedCode.startsWith('export', match.index ?? 0)) {
440
- continue;
441
- }
442
-
443
- const specifier = match.groups?.['specifier'];
444
- if (!specifier?.startsWith('.')) {
445
- continue;
446
- }
447
-
448
- const namedExports = match.groups?.['exports'] ?? '';
449
- for (const item of namedExports.split(',')) {
450
- const trimmedItem = item.trim();
451
- if (!trimmedItem) {
452
- continue;
453
- }
454
-
455
- const specifierTypeOnly =
456
- Boolean(match.groups?.['typeOnly']) || trimmedItem.startsWith('type ');
457
- const specifierText = trimmedItem.replace(/^type\s+/u, '');
458
- const exported =
459
- /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
460
- specifierText
461
- )?.groups;
462
- if (
463
- !exported?.['local'] ||
464
- (exported['name'] ?? exported['local']) !== identifier
465
- ) {
466
- continue;
467
- }
468
-
469
- exports.push({
470
- local: exported['local'],
471
- specifier,
472
- typeOnly: specifierTypeOnly,
473
- });
474
- }
475
- }
476
-
477
- return exports;
478
- };
479
-
480
- const localStarReexports = (
481
- source: string
482
- ): readonly { readonly specifier: string; readonly typeOnly: boolean }[] => {
483
- const code = maskSource(source, { strings: false });
484
- const stringsMaskedCode = maskSource(source, { strings: true });
485
- return [
486
- ...code.matchAll(
487
- /\bexport\s+(?<typeOnly>type\s+)?\*\s+from\s+['"](?<specifier>[^'"]+)['"]/gu
488
- ),
489
- ]
490
- .filter((match) => stringsMaskedCode.startsWith('export', match.index ?? 0))
491
- .map((match) => ({
492
- specifier: match.groups?.['specifier'] ?? '',
493
- typeOnly: Boolean(match.groups?.['typeOnly']),
494
- }))
495
- .filter((entry) => entry.specifier.startsWith('.'));
496
- };
497
-
498
- const resolveLocalModuleSpecifier = (
499
- sourcePath: string,
500
- specifier: string
501
- ): string | undefined => {
502
- const basePath = resolve(dirname(sourcePath), specifier);
503
- const candidates = [
504
- basePath,
505
- basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.ts` : undefined,
506
- basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.tsx` : undefined,
507
- `${basePath}.ts`,
508
- `${basePath}.tsx`,
509
- join(basePath, 'index.ts'),
510
- join(basePath, 'index.tsx'),
511
- ].filter((candidate): candidate is string => candidate !== undefined);
512
-
513
- return candidates.find((candidate) => existsSync(candidate));
514
- };
515
-
516
- const sourcePathExportsIdentifier = (
517
- sourcePath: string,
518
- identifier: string,
519
- expected: 'type' | 'value',
520
- visited = new Set<string>()
521
- ): boolean => {
522
- const normalizedSourcePath = realpathSync(sourcePath);
523
- const visitKey = `${normalizedSourcePath}:${identifier}:${expected}`;
524
- if (visited.has(visitKey)) {
525
- return false;
526
- }
527
- visited.add(visitKey);
528
-
529
- const source = readFileSync(normalizedSourcePath, 'utf8');
530
- if (sourceExportsIdentifier(source, identifier, expected)) {
531
- return true;
532
- }
533
-
534
- const sameFileLocal = sameFileExportListLocalForIdentifier(
535
- source,
536
- identifier,
537
- expected
538
- );
539
- if (sameFileLocal) {
540
- for (const localImport of localNamedImports(source, sameFileLocal)) {
541
- if (expected === 'value' && localImport.typeOnly) {
542
- continue;
543
- }
544
- const targetPath = resolveLocalModuleSpecifier(
545
- normalizedSourcePath,
546
- localImport.specifier
547
- );
548
- if (
549
- targetPath &&
550
- sourcePathExportsIdentifier(
551
- targetPath,
552
- localImport.imported,
553
- expected,
554
- visited
555
- )
556
- ) {
557
- return true;
558
- }
559
- }
560
- }
561
-
562
- for (const reexport of localNamedReexports(source, identifier)) {
563
- if (expected === 'value' && reexport.typeOnly) {
564
- continue;
565
- }
566
- const targetPath = resolveLocalModuleSpecifier(
567
- normalizedSourcePath,
568
- reexport.specifier
569
- );
570
- if (
571
- targetPath &&
572
- sourcePathExportsIdentifier(targetPath, reexport.local, expected, visited)
573
- ) {
574
- return true;
575
- }
576
- }
577
-
578
- for (const reexport of localStarReexports(source)) {
579
- if (expected === 'value' && reexport.typeOnly) {
580
- continue;
581
- }
582
- const targetPath = resolveLocalModuleSpecifier(
583
- normalizedSourcePath,
584
- reexport.specifier
585
- );
586
- if (
587
- targetPath &&
588
- sourcePathExportsIdentifier(targetPath, identifier, expected, visited)
589
- ) {
590
- return true;
591
- }
592
- }
593
-
594
- return false;
205
+ const relativeImportSpecifier = (fromFile: string, toFile: string): string => {
206
+ const withoutExtension = relative(dirname(fromFile), toFile)
207
+ .replaceAll('\\', '/')
208
+ .replace(/\.ts$/u, '.js');
209
+ return withoutExtension.startsWith('.')
210
+ ? withoutExtension
211
+ : `./${withoutExtension}`;
595
212
  };
596
213
 
597
214
  const assertHttpOwnerSupport = (
@@ -605,13 +222,22 @@ const assertHttpOwnerSupport = (
605
222
  }
606
223
 
607
224
  const requiredExports = [
608
- ['createFetchHandler', 'value'],
609
- ['CreateFetchHandlerOptions', 'type'],
225
+ ['createFetchHandler', 'value', false],
226
+ ['CreateFetchHandlerOptions', 'type', true],
610
227
  ] as const;
611
- const missing = requiredExports.filter(
612
- ([identifier, expected]) =>
613
- !sourcePathExportsIdentifier(rootExportTarget, identifier, expected)
614
- );
228
+ const missing = requiredExports.filter(([identifier, expected, typeOnly]) => {
229
+ if (typeOnly) {
230
+ const exportKind = adapterSourceExportKind(rootExportTarget, identifier);
231
+ return (
232
+ exportKind !== 'type' &&
233
+ exportKind !== 'interface-value' &&
234
+ exportKind !== 'interface-value-erased' &&
235
+ exportKind !== 'type-alias-value' &&
236
+ exportKind !== 'type-alias-value-erased'
237
+ );
238
+ }
239
+ return !adapterSourceExports(rootExportTarget, identifier, expected);
240
+ });
615
241
  if (missing.length === 0) {
616
242
  return Result.ok();
617
243
  }
@@ -766,6 +392,21 @@ export const createApp = (
766
392
  });
767
393
  `;
768
394
 
395
+ const generateHttpSubpathIndex = (supportImportPath: string): string =>
396
+ `import type { Topo } from '@ontrails/core';
397
+ import { createFetchHandler } from ${literal(supportImportPath)};
398
+ import type { CreateFetchHandlerOptions } from ${literal(supportImportPath)};
399
+
400
+ export interface CreateAppOptions extends CreateFetchHandlerOptions {}
401
+
402
+ export const createApp = (
403
+ graph: Topo,
404
+ options: CreateAppOptions = {}
405
+ ) => ({
406
+ fetch: createFetchHandler(graph, options),
407
+ });
408
+ `;
409
+
769
410
  const generateConformanceTest = (
770
411
  target: AdapterTargetCatalogEntry,
771
412
  adapterImport: string,
@@ -881,13 +522,128 @@ const buildExtractedPlan = (
881
522
  };
882
523
 
883
524
  const buildSubpathPlan = (
884
- _rootDir: string,
885
- _input: CreateAdapterInput,
886
- _target: AdapterTargetCatalogEntry
887
- ): Result<AdapterOperationPlan, Error> =>
888
- fail(
889
- 'Subpath adapter scaffolding is deferred until shared adapter checks discover subpath adapter subjects.'
525
+ rootDir: string,
526
+ input: CreateAdapterInput,
527
+ target: AdapterTargetCatalogEntry
528
+ ): Result<AdapterOperationPlan, Error> => {
529
+ if (input.packageName !== undefined) {
530
+ return fail(
531
+ 'packageName is only supported for extracted adapter placement.'
532
+ );
533
+ }
534
+
535
+ const ownerPackage = findWorkspacePackage<WorkspacePackageManifest>(
536
+ rootDir,
537
+ target.ownerPackage
538
+ );
539
+ if (!ownerPackage) {
540
+ return fail(
541
+ `Adapter target "${target.key}" owner package ${target.ownerPackage} is not a workspace package.`
542
+ );
543
+ }
544
+
545
+ const manifest = readJson<WorkspacePackageManifest>(
546
+ ownerPackage.packageJsonPath
547
+ );
548
+ if (!manifest || !isRecord(manifest)) {
549
+ return fail(
550
+ `Adapter target "${target.key}" owner package manifest could not be read.`
551
+ );
552
+ }
553
+
554
+ const manifestExports = manifest['exports'];
555
+ if (
556
+ manifestExports !== undefined &&
557
+ typeof manifestExports !== 'string' &&
558
+ !isRecord(manifestExports)
559
+ ) {
560
+ return fail(
561
+ `${target.ownerPackage} package.json exports must be an object before create.adapter can add a subpath adapter.`
562
+ );
563
+ }
564
+
565
+ const exportKey = `./${input.name}`;
566
+ const packageExports =
567
+ typeof manifestExports === 'string'
568
+ ? { '.': manifestExports }
569
+ : toMutableRecord(manifestExports);
570
+ if (Object.hasOwn(packageExports, exportKey)) {
571
+ return fail(`${target.ownerPackage} already exports "${exportKey}".`);
572
+ }
573
+
574
+ const manifestTrails = manifest['trails'];
575
+ const trails = toMutableRecord(manifestTrails);
576
+ if (manifestTrails !== undefined && !isRecord(manifestTrails)) {
577
+ return fail(
578
+ `${target.ownerPackage} package.json trails must be an object.`
579
+ );
580
+ }
581
+
582
+ const adapters = toMutableRecord(trails['adapters']);
583
+ if (trails['adapters'] !== undefined && !isRecord(trails['adapters'])) {
584
+ return fail(
585
+ `${target.ownerPackage} package.json trails.adapters must be an object before create.adapter can add a subpath adapter.`
586
+ );
587
+ }
588
+ if (Object.hasOwn(adapters, exportKey)) {
589
+ return fail(
590
+ `${target.ownerPackage} package.json trails.adapters already declares "${exportKey}".`
591
+ );
592
+ }
593
+
594
+ const sourcePath = `${ownerPackage.workspacePath}/src/${input.name}/index.ts`;
595
+ const sourceRoot = resolveProjectPath(
596
+ rootDir,
597
+ `${ownerPackage.workspacePath}/src/${input.name}`
890
598
  );
599
+ if (sourceRoot.isErr()) {
600
+ return sourceRoot;
601
+ }
602
+ if (existsSync(sourceRoot.value)) {
603
+ return fail(
604
+ `Subpath adapter source already exists at ${ownerPackage.workspacePath}/src/${input.name}.`
605
+ );
606
+ }
607
+
608
+ const supportTarget = packageRootExportTarget(target);
609
+ if (!supportTarget) {
610
+ return fail(
611
+ `Adapter target "${target.key}" cannot use the HTTP subpath template because ${target.ownerPackage} does not expose a readable package root export.`
612
+ );
613
+ }
614
+
615
+ packageExports[exportKey] = `./src/${input.name}/index.ts`;
616
+ adapters[exportKey] = { target: target.target };
617
+ trails['adapters'] = adapters;
618
+
619
+ const adapterImport = `${target.ownerPackage}/${input.name}`;
620
+ const sourceFile = resolve(rootDir, sourcePath);
621
+ const packageJson = formatJson({
622
+ ...manifest,
623
+ exports: packageExports,
624
+ trails,
625
+ });
626
+ const operations = [
627
+ writeOperation(`${ownerPackage.workspacePath}/package.json`, packageJson),
628
+ writeOperation(
629
+ sourcePath,
630
+ generateHttpSubpathIndex(
631
+ relativeImportSpecifier(sourceFile, supportTarget)
632
+ )
633
+ ),
634
+ writeOperation(
635
+ `${ownerPackage.workspacePath}/src/${input.name}/__tests__/conformance.test.ts`,
636
+ generateConformanceTest(target, adapterImport, '../index.js')
637
+ ),
638
+ ];
639
+
640
+ return Result.ok({
641
+ adapterImport,
642
+ operations,
643
+ packageName: adapterImport,
644
+ targetKey: target.key,
645
+ });
646
+ };
891
647
 
892
648
  const buildOperationPlan = (
893
649
  rootDir: string,
@@ -909,7 +665,24 @@ const runPlannedOperations = async (
909
665
 
910
666
  export const createAdapterTrail = trail('create.adapter', {
911
667
  args: ['name'],
912
- blaze: async (input: CreateAdapterInput, ctx) => {
668
+ description: 'Scaffold an adapter package from adapter target catalog facts',
669
+ fields: {
670
+ placement: {
671
+ options: [
672
+ {
673
+ hint: 'Standalone package under adapters/',
674
+ label: 'Extracted',
675
+ value: 'extracted',
676
+ },
677
+ {
678
+ hint: 'Owner package subpath export',
679
+ label: 'Subpath',
680
+ value: 'subpath',
681
+ },
682
+ ],
683
+ },
684
+ },
685
+ implementation: async (input: CreateAdapterInput, ctx) => {
913
686
  const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
914
687
  if (rootDirResult.isErr()) {
915
688
  return rootDirResult;
@@ -951,23 +724,11 @@ export const createAdapterTrail = trail('create.adapter', {
951
724
  diagnostics: [...report.diagnostics],
952
725
  dryRun: input.dryRun,
953
726
  packageName: plan.value.packageName,
954
- placement: 'extracted',
727
+ placement: input.placement,
955
728
  plannedOperations: [...plannedOperations.value],
956
729
  targetKey: plan.value.targetKey,
957
730
  } satisfies CreateAdapterResult);
958
731
  },
959
- description: 'Scaffold an adapter package from adapter target catalog facts',
960
- fields: {
961
- placement: {
962
- options: [
963
- {
964
- hint: 'Standalone package under adapters/',
965
- label: 'Extracted',
966
- value: 'extracted',
967
- },
968
- ],
969
- },
970
- },
971
732
  input: z.object({
972
733
  dryRun: z
973
734
  .boolean()