aw-spec-cli 0.1.26 → 0.1.27
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/assets/.agents/skills/appweaver/SKILL.md +7 -47
- package/assets/.appweaver/ORCH-DIALECT.md +13 -9
- package/assets/.appweaver/RULE-PARTITION.md +3 -3
- package/assets/.appweaver/STACK-PROFILES.md +188 -101
- package/assets/.appweaver/domain.json +1 -1
- package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +38 -10
- package/assets/.appweaver/orchestrator/LAUNCHER.md +88 -0
- package/assets/.appweaver/orchestrator/README.md +120 -53
- package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +1 -1
- package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +6 -4
- package/assets/.appweaver/orchestrator/modules/foundation-lane.orch.yaml +237 -0
- package/assets/.appweaver/orchestrator/modules/foundation-pack.orch.yaml +349 -0
- package/assets/.appweaver/orchestrator/modules/foundation-spec.orch.yaml +381 -0
- package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +4 -4
- package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +3 -3
- package/assets/.appweaver/orchestrator/orchestrator.py +162 -63
- package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +2 -2
- package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +5 -5
- package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +1 -1
- package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +14 -14
- package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +2 -2
- package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
- package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +3 -3
- package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +41 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +13 -13
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +3 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +3 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +4 -4
- package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +2 -2
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +61 -12
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +172 -293
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +29 -28
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +53 -53
- package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +2 -2
- package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +3 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +4 -4
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-bounded-context.orch.yaml +26 -2
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +21 -15
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +14 -14
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +2 -2
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +3 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +6 -6
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +284 -128
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +224 -70
- package/assets/.appweaver/scripts/python/aw-foundation.py +809 -20
- package/assets/.appweaver/scripts/python/aw-inventory.py +28 -21
- package/assets/.appweaver/scripts/python/aw-plan-coverage.py +2 -2
- package/assets/.appweaver/scripts/python/aw-rev-constitution.py +6 -6
- package/assets/.appweaver/scripts/python/aw-stack.py +195 -89
- package/assets/.appweaver/stack-profiles/angular.json +2 -1
- package/assets/.appweaver/stack-profiles/blazor-wasm.json +2 -1
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +16 -3
- package/assets/.appweaver/stack-profiles/go-chi.json +6 -1
- package/assets/.appweaver/stack-profiles/java-spring.json +12 -5
- package/assets/.appweaver/stack-profiles/nextjs.json +2 -1
- package/assets/.appweaver/stack-profiles/node-express.json +8 -2
- package/assets/.appweaver/stack-profiles/python-fastapi.json +9 -2
- package/assets/.appweaver/stack-profiles/react-vite.json +2 -1
- package/assets/.appweaver/stack-profiles/sveltekit.json +2 -1
- package/assets/.appweaver/stack-profiles/vue-vite.json +2 -1
- package/assets/.appweaver/stacks.json +37 -1
- package/assets/.appweaver/templates/constitution-ui-template.md +2 -2
- package/assets/.claude/skills/appweaver/SKILL.md +7 -46
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.rules.json +31 -0
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.rules.json +16 -0
- package/assets/foundations/foundation.spec.template.txt +73 -0
- package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +1 -1
- package/assets/foundations/java/Hexagonal/unpack-foundation.sh +4 -5
- package/assets/foundations/node/NextAppRouter/NextAppRouter.spec.txt +68 -0
- package/assets/guardrails/README.md +31 -17
- package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain-rule-bindings.md +5 -5
- package/assets/guardrails/{projects/dotnet-default-project → project-specific}/tech-rule-bindings.md +151 -114
- package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +58 -4
- package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +1 -1
- package/assets/guardrails/technology/dotnet-webapi/testing.md +1 -1
- package/assets/guardrails/technology/nextjs/api-client.md +1 -1
- package/assets/guardrails/technology/nextjs/component-architecture.md +1 -1
- package/assets/guardrails/technology/nextjs/component-conventions.md +2 -2
- package/assets/guardrails/technology/nextjs/design-system.md +1 -1
- package/assets/guardrails/technology/nextjs/frontend-testing.md +1 -1
- package/assets/guardrails/technology/nextjs/routing-navigation.md +1 -1
- package/assets/guardrails/technology/nextjs/ui-constitution.md +3 -3
- package/package.json +1 -1
- package/assets/.agents/skills/appweaver-demo/SKILL.md +0 -59
- package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +0 -6
- package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -100
- package/assets/.claude/skills/appweaver-demo/SKILL.md +0 -59
- package/assets/.kilocode/agent.json +0 -28
- package/assets/.kilocode/workflows/appweaver-demo.md +0 -59
- package/assets/.kilocode/workflows/appweaver.md +0 -55
- /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/domain/domain-requirements.md +0 -0
- /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/frontend/ui-requirements.md +0 -0
- /package/assets/guardrails/{projects/dotnet-default-project → project-specific}/service/technical-requirements.md +0 -0
|
@@ -21,23 +21,26 @@ stays optional in the schema for a future caller that proposes a lane without a
|
|
|
21
21
|
person in the loop; a reader that branches on it keeps working either way.
|
|
22
22
|
|
|
23
23
|
A person editing stacks.json in a text editor is the other way a lane arrives,
|
|
24
|
-
and it is supported: the kit ships the file
|
|
25
|
-
that
|
|
24
|
+
and it is supported: the kit ships the file with an empty `lanes` array for exactly
|
|
25
|
+
that, plus a `_lane_format` key holding one sample lane of each kind with every field
|
|
26
|
+
filled in. Nothing reads `_lane_format`; it is there to copy from. It is the one path that does not pass the writer's checks, which is why
|
|
26
27
|
`verify` re-checks the roots-disjoint invariant rather than trusting that
|
|
27
28
|
`declare` already did. `resolve` can never write, which is why these are two
|
|
28
29
|
subcommands and not one with a mode flag.
|
|
29
30
|
|
|
30
|
-
A lane's `root` IS
|
|
31
|
+
A lane's `root` IS the lane root folder — the subtree the lane owns and the only place
|
|
31
32
|
its implement phase may write. The two words name one field, and `--source-path`
|
|
32
33
|
is accepted wherever `--root` is.
|
|
33
34
|
|
|
34
|
-
WHERE THE COMMANDS RUN is a separate question, because
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
WHERE THE COMMANDS RUN is a separate question, because the answer can be a different
|
|
36
|
+
directory: a classic .NET repository keeps its solution file at the top while the
|
|
37
|
+
lane owns `src/`. `run_from` carries that answer — "repo" (the default, and the shape
|
|
38
|
+
of every lane written before the key existed) or "root". A stack whose build file
|
|
39
|
+
lives INSIDE its subtree needs "root" and cannot work without it: a Maven wrapper at
|
|
40
|
+
backend/<app>/mvnw is unreachable from the repository root. A NEW project is one
|
|
41
|
+
self-contained folder per lane — every profile defaults to `backend/` or `frontend/`
|
|
42
|
+
with `run_from: "root"` — while `declare` answers for code that already exists from
|
|
43
|
+
where its build file actually sits (see `build_files_at`).
|
|
41
44
|
|
|
42
45
|
WHERE THE PATH IS CHECKED AGAINST THE DISK. `declare` records a path; it does not
|
|
43
46
|
require the path to exist, because in a greenfield project it does not exist yet —
|
|
@@ -270,6 +273,11 @@ def detect_profiles(repo, kind=None, exclude_roots=()):
|
|
|
270
273
|
files_for_prof = scoped if prof.get("kind") != "frontend" else files
|
|
271
274
|
evidence, matched, anchored_hits = [], 0, 0
|
|
272
275
|
prof_root = norm_root(prof.get("root") or "")
|
|
276
|
+
# The `detect` patterns are written against the layout an EXISTING repository of
|
|
277
|
+
# this stack usually has (`src/**/*.csproj`, `web/next.config.*`), which is the
|
|
278
|
+
# profile's `conventional_root` — not its `root`, the folder a NEW project gets.
|
|
279
|
+
conv_root = norm_root(prof.get("conventional_root") or "")
|
|
280
|
+
bases = [b for b in (prof_root, conv_root) if b and b != REPO_ROOT]
|
|
273
281
|
for pat in patterns:
|
|
274
282
|
rx = _glob_rx(pat)
|
|
275
283
|
rx_any = _glob_rx("**/" + pat)
|
|
@@ -278,8 +286,8 @@ def detect_profiles(repo, kind=None, exclude_roots=()):
|
|
|
278
286
|
anchored_hits += 1
|
|
279
287
|
else:
|
|
280
288
|
hits = [f for f in files_for_prof if rx_any.match(f)]
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
base = next((b for b in bases if pat.startswith(b)), "")
|
|
290
|
+
if not hits and base and os.path.basename(pat) not in ambiguous:
|
|
283
291
|
# RELOCATED, with the profile's own root segment substituted rather than
|
|
284
292
|
# prefixed. Prefixing alone only ever finds the profile's own folder name
|
|
285
293
|
# deeper in the tree — `apps/portal/web/angular.json` — so a UI in
|
|
@@ -288,7 +296,7 @@ def detect_profiles(repo, kind=None, exclude_roots=()):
|
|
|
288
296
|
# Restricted to names only one kind claims, so substitution widens where a
|
|
289
297
|
# stack may live without widening what counts as evidence for it.
|
|
290
298
|
hits = [f for f in files_for_prof
|
|
291
|
-
if _glob_rx("**/" + pat[len(
|
|
299
|
+
if _glob_rx("**/" + pat[len(base):]).match(f)]
|
|
292
300
|
if not hits:
|
|
293
301
|
continue
|
|
294
302
|
matched += 1
|
|
@@ -298,20 +306,21 @@ def detect_profiles(repo, kind=None, exclude_roots=()):
|
|
|
298
306
|
dirs = sorted({os.path.dirname(f) for f in evidence}, key=lambda d: (d.count("/"), len(d)))
|
|
299
307
|
# WHERE THE SOURCE LIVES, which is not always where the evidence sits. A manifest
|
|
300
308
|
# at the repo root — `App.sln`, a workspace `package.json` — marks the repository,
|
|
301
|
-
# not the subtree: the code it describes is under the profile's
|
|
302
|
-
# root evidence defers to that root when the directory is really there, and only
|
|
309
|
+
# not the subtree: the code it describes is under one of the profile's roots. So
|
|
310
|
+
# repo-root evidence defers to that root when the directory is really there, and only
|
|
303
311
|
# falls back to the repo root itself when it is not, which is the case for the
|
|
304
312
|
# single-package layouts (`create-next-app`, a UI checked out at the top level)
|
|
305
|
-
# whose files genuinely are at the top.
|
|
313
|
+
# whose files genuinely are at the top. The conventional root is tried first:
|
|
314
|
+
# repo-root evidence (`App.sln`) is the classic layout, whose code sits under
|
|
315
|
+
# `src/` — not under the `backend/` a new project would get.
|
|
306
316
|
prof_default = norm_root(prof.get("root") or "")
|
|
307
317
|
if not dirs:
|
|
308
318
|
root = prof_default
|
|
309
319
|
elif dirs[0]:
|
|
310
320
|
root = norm_root(dirs[0])
|
|
311
|
-
elif prof_default and os.path.isdir(os.path.join(repo, prof_default)):
|
|
312
|
-
root = prof_default
|
|
313
321
|
else:
|
|
314
|
-
root =
|
|
322
|
+
root = next((r for r in (conv_root, prof_default)
|
|
323
|
+
if r and os.path.isdir(os.path.join(repo, r))), REPO_ROOT)
|
|
315
324
|
out.append({
|
|
316
325
|
"id": prof["id"],
|
|
317
326
|
"kind": prof.get("kind", ""),
|
|
@@ -404,7 +413,7 @@ def cmd_list(args):
|
|
|
404
413
|
repository looks like. Two subcommands would walk the file tree twice and add
|
|
405
414
|
two steps to a workflow whose whole value is being short. One call returns the
|
|
406
415
|
options, the profile the files point at, and the directory the evidence sits
|
|
407
|
-
in — which is the
|
|
416
|
+
in — which is the lane root folder the operator is about to be asked to confirm.
|
|
408
417
|
|
|
409
418
|
HOW A DETECTED STACK IS PROPOSED, and why it is not preselected. Detection
|
|
410
419
|
moves its match to the FRONT of the option list; it never fills `preselect`.
|
|
@@ -448,10 +457,10 @@ def cmd_list(args):
|
|
|
448
457
|
preselect = resolve_preselect(args.preselect, profiles, args.include_none, args.kind)
|
|
449
458
|
preselect_source = "argument" if preselect else ""
|
|
450
459
|
|
|
451
|
-
# Candidate
|
|
460
|
+
# Candidate LANE ROOT FOLDERS for the follow-up question, best first. Labelled with why
|
|
452
461
|
# each is offered, because the operator picking from this list is choosing the
|
|
453
462
|
# subtree a generator will be allowed to write into. Free text is always
|
|
454
|
-
# accepted — a repo that matches no profile still has a
|
|
463
|
+
# accepted — a repo that matches no profile still has a lane root folder.
|
|
455
464
|
#
|
|
456
465
|
# WITH --profile the stack is already chosen, and the list answers for it alone:
|
|
457
466
|
# where THAT stack's evidence sits, then its own default, then where another
|
|
@@ -460,7 +469,7 @@ def cmd_list(args):
|
|
|
460
469
|
# operator did not choose.
|
|
461
470
|
#
|
|
462
471
|
# WITHOUT --profile the list is built before any choice, and a default root is
|
|
463
|
-
# usually shared (
|
|
472
|
+
# usually shared (every frontend profile defaults to frontend/), so its label names every
|
|
464
473
|
# profile that proposes it rather than whichever one sorts first.
|
|
465
474
|
chosen = None
|
|
466
475
|
if args.profile:
|
|
@@ -541,16 +550,16 @@ def strip_root_label(value):
|
|
|
541
550
|
return (value or "").split(ROOT_LABEL_SEP, 1)[0].strip()
|
|
542
551
|
|
|
543
552
|
|
|
544
|
-
# The repository root as a
|
|
553
|
+
# The repository root as a lane root folder. A single-module project — a Maven, Cargo, Go
|
|
545
554
|
# or Node repo whose manifest sits at the top — has no subdirectory to name, and the
|
|
546
|
-
# empty string cannot carry it: `verify` reads an empty root as "no
|
|
555
|
+
# empty string cannot carry it: `verify` reads an empty root as "no lane root folder
|
|
547
556
|
# declared". "./" is the one spelling that survives being normalised twice, which is
|
|
548
557
|
# what makes it usable as a STORED value rather than only as typed input.
|
|
549
558
|
REPO_ROOT = "./"
|
|
550
559
|
|
|
551
560
|
|
|
552
561
|
def norm_root(root):
|
|
553
|
-
"""Canonicalise a
|
|
562
|
+
"""Canonicalise a lane root folder, IDEMPOTENTLY.
|
|
554
563
|
|
|
555
564
|
Idempotence is a correctness requirement, not tidiness: `declare` normalises what it
|
|
556
565
|
writes and `verify` normalises what it reads, so a value that changes on the second
|
|
@@ -575,7 +584,7 @@ def norm_root(root):
|
|
|
575
584
|
|
|
576
585
|
|
|
577
586
|
def root_on_disk(repo, root):
|
|
578
|
-
"""Does the lane
|
|
587
|
+
"""Does the lane root folder exist as a directory?
|
|
579
588
|
|
|
580
589
|
Reported everywhere a root is reported and enforced only by `verify`. A false
|
|
581
590
|
here is an ordinary state before any code is written and a hard error once a
|
|
@@ -588,14 +597,15 @@ def root_on_disk(repo, root):
|
|
|
588
597
|
def lane_workdir(repo, lane):
|
|
589
598
|
"""The directory a lane's commands run in.
|
|
590
599
|
|
|
591
|
-
`run_from: "root"` runs them in the lane
|
|
600
|
+
`run_from: "root"` runs them in the lane root folder; anything else — including
|
|
592
601
|
the key being ABSENT, which is the shape of every lane written before this key
|
|
593
|
-
existed — runs them at the repository root. That default is not laziness: a
|
|
594
|
-
solution file, a go.mod or a package.json at the top is what
|
|
595
|
-
`go build ./...` and `npm run build` need to find,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
602
|
+
existed — runs them at the repository root. That default is not laziness: in a
|
|
603
|
+
classic repository a .NET solution file, a go.mod or a package.json at the top is what
|
|
604
|
+
`dotnet build`, `go build ./...` and `npm run build` need to find, while the lane is
|
|
605
|
+
the SOURCE directory under it. The two coincide for a stack whose build file lives
|
|
606
|
+
inside the subtree — a Maven wrapper at backend/orderservice/mvnw cannot be reached
|
|
607
|
+
from the repo root at all — and for every new project, which is one self-contained
|
|
608
|
+
folder per lane.
|
|
599
609
|
|
|
600
610
|
Falls back to the repo root when the declared root is not on disk, so the phase
|
|
601
611
|
reports the command's own failure instead of a chdir error.
|
|
@@ -608,27 +618,108 @@ def lane_workdir(repo, lane):
|
|
|
608
618
|
return repo
|
|
609
619
|
|
|
610
620
|
|
|
611
|
-
def
|
|
612
|
-
"""
|
|
621
|
+
def build_files_at(repo, root, build_files):
|
|
622
|
+
"""Where a stack's build file already sits for a lane at `root`: "root" when one is
|
|
623
|
+
directly inside the lane root folder, "repo" when one is only at the repository root,
|
|
624
|
+
"" when neither — nothing is on disk yet, so the caller falls back to the profile.
|
|
625
|
+
|
|
626
|
+
This is what lets one profile serve both kinds of repository. A new project is one
|
|
627
|
+
self-contained folder per lane (`backend/api.sln`), and its commands run inside it; a
|
|
628
|
+
classic .NET repository keeps `api.sln` at the top with the code under `src/`, and its
|
|
629
|
+
commands have to run at the top. Reading the answer off the disk at declare time gives
|
|
630
|
+
each its own, where a fixed profile default would break one of them.
|
|
631
|
+
"""
|
|
632
|
+
if not build_files:
|
|
633
|
+
return ""
|
|
634
|
+
r = norm_root(root)
|
|
635
|
+
lane_dir = repo if (not r or r == REPO_ROOT) else os.path.join(repo, r.rstrip("/"))
|
|
636
|
+
|
|
637
|
+
def has(d):
|
|
638
|
+
return any(os.path.isfile(p) for pat in build_files
|
|
639
|
+
for p in glob.glob(os.path.join(d, pat)))
|
|
640
|
+
|
|
641
|
+
if os.path.isdir(lane_dir) and has(lane_dir):
|
|
642
|
+
return "root"
|
|
643
|
+
if has(repo):
|
|
644
|
+
return "repo"
|
|
645
|
+
return ""
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
# Folders a runnable project is never looked for in: build output, dependencies, tests.
|
|
649
|
+
HOST_SKIP_DIRS = {"bin", "obj", "node_modules", ".git", ".vs", "packages", "tests", "test"}
|
|
613
650
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
651
|
+
|
|
652
|
+
def lane_host(repo, lane):
|
|
653
|
+
"""The lane's runnable project folder, which `{{host}}` in a command stands for.
|
|
654
|
+
|
|
655
|
+
A lane root folder often holds several projects and only one of them is the thing to
|
|
656
|
+
run — a .NET solution's Aspire AppHost, or its API when there is no AppHost. The
|
|
657
|
+
profile's `host_patterns` say where that project sits, best first, as paths relative
|
|
658
|
+
to the lane root folder (`**/` for any depth, case ignored). The first pattern that
|
|
659
|
+
matches exactly ONE file names the project; its folder is returned as seen from where
|
|
660
|
+
the lane's commands run. None when the profile has no patterns, the lane root folder is
|
|
661
|
+
not on disk, or no pattern has a single match — the caller then uses the lane root folder.
|
|
662
|
+
"""
|
|
663
|
+
if not repo:
|
|
664
|
+
return None
|
|
665
|
+
patterns = [p.lower() for p in profile_of(repo, lane).get("host_patterns") or []]
|
|
666
|
+
r = norm_root(lane.get("root"))
|
|
667
|
+
lane_dir = repo if (not r or r == REPO_ROOT) else os.path.join(repo, r.rstrip("/"))
|
|
668
|
+
if not patterns or not os.path.isdir(lane_dir):
|
|
669
|
+
return None
|
|
670
|
+
leaves = {p.rsplit("/", 1)[-1] for p in patterns}
|
|
671
|
+
files = []
|
|
672
|
+
for dirpath, dirnames, filenames in os.walk(lane_dir):
|
|
673
|
+
dirnames[:] = [d for d in dirnames if d.lower() not in HOST_SKIP_DIRS]
|
|
674
|
+
for name in filenames:
|
|
675
|
+
if any(fnmatch.fnmatchcase(name.lower(), leaf) for leaf in leaves):
|
|
676
|
+
rel = os.path.relpath(os.path.join(dirpath, name), lane_dir).replace("\\", "/")
|
|
677
|
+
files.append(rel)
|
|
678
|
+
for pat in patterns:
|
|
679
|
+
bare = pat[3:] if pat.startswith("**/") else pat
|
|
680
|
+
hits = [f for f in files
|
|
681
|
+
if fnmatch.fnmatchcase(f.lower(), pat) or fnmatch.fnmatchcase(f.lower(), bare)]
|
|
682
|
+
if len(hits) == 1:
|
|
683
|
+
project_dir = os.path.join(lane_dir, os.path.dirname(hits[0]))
|
|
684
|
+
rel = os.path.relpath(project_dir, lane_workdir(repo, lane)).replace("\\", "/")
|
|
685
|
+
return rel
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def lane_command(lane, phase, repo=None):
|
|
690
|
+
"""The command for one phase, with `{{root}}` standing for the lane root folder and
|
|
691
|
+
`{{host}}` for the lane's runnable project inside it (see lane_host).
|
|
692
|
+
|
|
693
|
+
A profile command sometimes has to NAME the lane root folder — `python -m
|
|
694
|
+
compileall app`. Written literally, that is the profile's default root frozen into
|
|
695
|
+
the command, and it stays frozen when the lane declares a different one: a service
|
|
696
|
+
at `Services/Api/` compiles the code at `app/`, or fails looking for it. The
|
|
697
|
+
placeholder lets the profile say "the root this lane declares" and be right on
|
|
698
|
+
every layout.
|
|
620
699
|
|
|
621
700
|
Expanded at RUN time, not at declare time, so editing `root` in stacks.json by
|
|
622
701
|
hand keeps the command consistent with it. A command with no placeholder is
|
|
623
702
|
returned untouched, so every literal command already on disk is unaffected.
|
|
703
|
+
|
|
704
|
+
The root is named as seen from where the command RUNS: a `run_from: "root"` lane
|
|
705
|
+
runs inside its root, so there `{{root}}` is `.` — the repository-relative path
|
|
706
|
+
would point at `backend/backend`.
|
|
707
|
+
|
|
708
|
+
`{{host}}` exists because the lane root folder is not always runnable: `dotnet run
|
|
709
|
+
--project <folder>` needs exactly one project in that folder, and a solution has
|
|
710
|
+
several. It is looked up when the command is read, so a project scaffolded after the
|
|
711
|
+
lane was declared is found; with no single match it falls back to `{{root}}`.
|
|
624
712
|
"""
|
|
625
713
|
cmd = (lane.get(phase) or "").strip()
|
|
714
|
+
if "{{host}}" in cmd:
|
|
715
|
+
cmd = cmd.replace("{{host}}", lane_host(repo, lane) or "{{root}}")
|
|
626
716
|
if "{{root}}" not in cmd:
|
|
627
717
|
return cmd
|
|
628
718
|
r = norm_root(lane.get("root"))
|
|
629
719
|
# `./` is the repo root: a bare `./` reads as noise in a command line, and the
|
|
630
720
|
# only sensible substitution for "the root, which is here" is the cwd itself.
|
|
631
|
-
|
|
721
|
+
runs_inside = (lane.get("run_from") or "repo") == "root"
|
|
722
|
+
sub = "." if (r == REPO_ROOT or not r or runs_inside) else r.rstrip("/")
|
|
632
723
|
return cmd.replace("{{root}}", sub)
|
|
633
724
|
|
|
634
725
|
|
|
@@ -654,7 +745,7 @@ def lane_test_path(repo, lane, prof=None):
|
|
|
654
745
|
|
|
655
746
|
Most backend stacks keep tests BESIDE the source rather than in it: .NET's `tests/`
|
|
656
747
|
next to `src/`, Maven's `src/test/java/` next to `src/main/java/`, pytest's `tests/`
|
|
657
|
-
next to `app/`. A lane that owned only its
|
|
748
|
+
next to `app/`. A lane that owned only its lane root folder could not hold its own tests.
|
|
658
749
|
|
|
659
750
|
Answered at READ time, like `ecosystem`, so a lane declared before this existed has
|
|
660
751
|
one without being re-declared:
|
|
@@ -662,8 +753,11 @@ def lane_test_path(repo, lane, prof=None):
|
|
|
662
753
|
does not describe;
|
|
663
754
|
- else the profile's `test_root`, which pairs with the profile's default `root`. A
|
|
664
755
|
lane at that root takes it as is; a lane whose root ends with it takes it under
|
|
665
|
-
the same prefix (`backend
|
|
666
|
-
- else the
|
|
756
|
+
the same prefix (`apps/backend/` ⇒ `apps/backend/tests/`);
|
|
757
|
+
- else the profile's `conventional_test_root`, which pairs the same way with its
|
|
758
|
+
`conventional_root` — the classic layout an existing repository has (`src/` ⇒
|
|
759
|
+
`tests/`), so a lane declared at it keeps the test path it always had;
|
|
760
|
+
- else the tests live inside the lane root folder. That is right for every frontend
|
|
667
761
|
profile, for Go's colocated `_test.go` files, and for a Maven lane rooted at the
|
|
668
762
|
directory holding its pom.xml.
|
|
669
763
|
"""
|
|
@@ -676,24 +770,25 @@ def lane_test_path(repo, lane, prof=None):
|
|
|
676
770
|
if own:
|
|
677
771
|
return own
|
|
678
772
|
prof = profile_of(repo, lane) if prof is None else prof
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
773
|
+
for key_root, key_tests in (("root", "test_root"),
|
|
774
|
+
("conventional_root", "conventional_test_root")):
|
|
775
|
+
base, tests = norm_root(prof.get(key_root)), norm_root(prof.get(key_tests))
|
|
776
|
+
if not base or not tests or REPO_ROOT in (base, tests):
|
|
777
|
+
continue
|
|
778
|
+
if root == base:
|
|
779
|
+
return tests
|
|
780
|
+
if root.endswith("/" + base):
|
|
781
|
+
return root[: -len(base)] + tests
|
|
687
782
|
return root
|
|
688
783
|
|
|
689
784
|
|
|
690
785
|
def lane_owned_paths(repo, lane, prof=None):
|
|
691
|
-
"""Every subtree a lane owns, labelled: its
|
|
786
|
+
"""Every subtree a lane owns, labelled: its lane root folder, and its test path when that
|
|
692
787
|
is a different directory. Ownership checks compare these, so a test path can never
|
|
693
788
|
land inside another lane's subtree."""
|
|
694
789
|
root = norm_root(lane.get("root"))
|
|
695
790
|
tests = lane_test_path(repo, lane, prof)
|
|
696
|
-
owned = [("
|
|
791
|
+
owned = [("lane root folder", root)] if root else []
|
|
697
792
|
if tests and tests != root:
|
|
698
793
|
owned.append(("test path", tests))
|
|
699
794
|
return owned
|
|
@@ -768,7 +863,7 @@ def cmd_declare(args):
|
|
|
768
863
|
lane_id = args.id or DEFAULT_LANE_ID.get(kind, kind)
|
|
769
864
|
root = norm_root(args.root or (prof or {}).get("root") or "")
|
|
770
865
|
if not root:
|
|
771
|
-
fail("lane-declare: lane '%s' has no
|
|
866
|
+
fail("lane-declare: lane '%s' has no lane root folder. A lane without a root owns "
|
|
772
867
|
"nothing, so nothing can be built, run or written into it. Pass "
|
|
773
868
|
"--source-path <dir> (the profile declares no default)." % lane_id)
|
|
774
869
|
|
|
@@ -777,7 +872,7 @@ def cmd_declare(args):
|
|
|
777
872
|
# is about to read the code out of it.
|
|
778
873
|
exists = root_on_disk(repo, root)
|
|
779
874
|
if args.require_existing_root and not exists:
|
|
780
|
-
fail("lane-declare:
|
|
875
|
+
fail("lane-declare: lane root folder '%s' for lane '%s' does not exist under %s. "
|
|
781
876
|
"Point the lane at the directory that actually holds this lane's source, "
|
|
782
877
|
"or re-run /appweaver select-stack to correct it."
|
|
783
878
|
% (root, lane_id, repo))
|
|
@@ -802,8 +897,12 @@ def cmd_declare(args):
|
|
|
802
897
|
lane["migrate"] = migrate
|
|
803
898
|
# WHERE this lane's commands run — see lane_workdir. Recorded only when it is not
|
|
804
899
|
# the default, so a lane reads the same as it always did unless its stack needs the
|
|
805
|
-
# other answer.
|
|
806
|
-
|
|
900
|
+
# other answer. An explicit --run-from wins; else the stack's build file decides when
|
|
901
|
+
# it is on disk (build_files_at); else — a greenfield lane, nothing written yet — the
|
|
902
|
+
# profile's own answer, which for a new project is inside the lane.
|
|
903
|
+
run_from = (getattr(args, "run_from", "")
|
|
904
|
+
or build_files_at(repo, root, (prof or {}).get("build_files") or [])
|
|
905
|
+
or (prof or {}).get("run_from", ""))
|
|
807
906
|
if run_from == "root":
|
|
808
907
|
lane["run_from"] = "root"
|
|
809
908
|
# What the code was found to be, when no shipped profile names it.
|
|
@@ -824,7 +923,7 @@ def cmd_declare(args):
|
|
|
824
923
|
for theirs_label, theirs in lane_owned_paths(repo, other):
|
|
825
924
|
if roots_overlap(mine, theirs):
|
|
826
925
|
fail("lane-declare: %s '%s' for lane '%s' overlaps lane '%s''s %s '%s'. "
|
|
827
|
-
"A lane owns its
|
|
926
|
+
"A lane owns its lane root folder and its test path, and no two lanes "
|
|
828
927
|
"may share a subtree."
|
|
829
928
|
% (mine_label, mine, lane_id, other.get("id"), theirs_label, theirs))
|
|
830
929
|
|
|
@@ -893,7 +992,7 @@ def profile_of(repo, lane):
|
|
|
893
992
|
def kind_block(repo, lanes, kind):
|
|
894
993
|
"""Every field a caller needs about ONE kind, flattened and prefixed.
|
|
895
994
|
|
|
896
|
-
A workflow that wants the service
|
|
995
|
+
A workflow that wants the service lane root folder binds `$.backend_source_path`
|
|
897
996
|
and is done — no `--kind` argument, no second call, and no branch on which
|
|
898
997
|
lane came back. Both kinds are always present in the payload, so the two-lane
|
|
899
998
|
steps (a gate that needs backend, a report that names both) resolve one script
|
|
@@ -927,14 +1026,21 @@ def kind_block(repo, lanes, kind):
|
|
|
927
1026
|
kind + "_root": root,
|
|
928
1027
|
kind + "_source_path": root,
|
|
929
1028
|
kind + "_root_exists": root_on_disk(repo, root),
|
|
930
|
-
# Where the lane's paired tests go. Equal to the
|
|
1029
|
+
# Where the lane's paired tests go. Equal to the lane root folder when the stack keeps
|
|
931
1030
|
# its tests inside it; see lane_test_path.
|
|
932
1031
|
kind + "_test_path": test_path,
|
|
933
1032
|
kind + "_test_path_exists": root_on_disk(repo, test_path),
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1033
|
+
# Where the lane's commands run, as DECLARED — `./` for the repository root.
|
|
1034
|
+
# Read from the declaration rather than from lane_workdir, which falls back to the
|
|
1035
|
+
# repo root while the lane root folder is not on disk yet; a phase that creates the
|
|
1036
|
+
# project has to be told where its build will run before that path exists.
|
|
1037
|
+
kind + "_workdir": (norm_root(root) or REPO_ROOT)
|
|
1038
|
+
if lane and (lane.get("run_from") or "repo") == "root"
|
|
1039
|
+
else (REPO_ROOT if lane else ""),
|
|
1040
|
+
kind + "_build": lane_command(lane, "build", repo),
|
|
1041
|
+
kind + "_test": lane_command(lane, "test", repo),
|
|
1042
|
+
kind + "_run": lane_command(lane, "run", repo),
|
|
1043
|
+
kind + "_migrate": lane_command(lane, "migrate", repo) or prof.get("migrate", ""),
|
|
938
1044
|
}
|
|
939
1045
|
|
|
940
1046
|
|
|
@@ -956,7 +1062,7 @@ def lane_options(repo, lanes):
|
|
|
956
1062
|
title = (prof.get("title", "") or lane.get("stack", "")
|
|
957
1063
|
or lane.get("profile", "") or "no stack declared")
|
|
958
1064
|
root = norm_root(lane.get("root"))
|
|
959
|
-
where = "the repo root" if root == REPO_ROOT else (root or "no
|
|
1065
|
+
where = "the repo root" if root == REPO_ROOT else (root or "no lane root folder")
|
|
960
1066
|
label = "%s %s at %s" % (lane.get("kind", "lane"), title, where)
|
|
961
1067
|
tests = lane_test_path(repo, lane, prof)
|
|
962
1068
|
if tests and tests != root:
|
|
@@ -1029,7 +1135,7 @@ def cmd_resolve(args):
|
|
|
1029
1135
|
lanes = data.get("lanes") or []
|
|
1030
1136
|
match = {}
|
|
1031
1137
|
# The id may arrive as a whole option line — `user_select` hands back the string it
|
|
1032
|
-
# displayed, label and all — so strip it the way a
|
|
1138
|
+
# displayed, label and all — so strip it the way a lane root folder is stripped.
|
|
1033
1139
|
wanted = strip_root_label(args.id) if args.id else ""
|
|
1034
1140
|
if wanted:
|
|
1035
1141
|
match = next((l for l in lanes if l.get("id") == wanted), {})
|
|
@@ -1052,10 +1158,10 @@ def cmd_resolve(args):
|
|
|
1052
1158
|
"source_path": match.get("root", ""),
|
|
1053
1159
|
"test_path": lane_test_path(args.repo, match),
|
|
1054
1160
|
"root_exists": root_on_disk(args.repo, match.get("root", "")),
|
|
1055
|
-
"build": lane_command(match, "build"),
|
|
1056
|
-
"test": lane_command(match, "test"),
|
|
1057
|
-
"run": lane_command(match, "run"),
|
|
1058
|
-
"migrate": lane_command(match, "migrate") or profile_of(args.repo, match).get("migrate", ""),
|
|
1161
|
+
"build": lane_command(match, "build", args.repo),
|
|
1162
|
+
"test": lane_command(match, "test", args.repo),
|
|
1163
|
+
"run": lane_command(match, "run", args.repo),
|
|
1164
|
+
"migrate": lane_command(match, "migrate", args.repo) or profile_of(args.repo, match).get("migrate", ""),
|
|
1059
1165
|
"language": profile_of(args.repo, match).get("language", ""),
|
|
1060
1166
|
"ecosystem": profile_of(args.repo, match).get("ecosystem", ""),
|
|
1061
1167
|
"prototype": data.get("prototype", {}),
|
|
@@ -1077,7 +1183,7 @@ def cmd_resolve(args):
|
|
|
1077
1183
|
# only for the kinds a caller asks for with --require, because "no frontend
|
|
1078
1184
|
# lane" is the correct and common state and must never fail a backend build.
|
|
1079
1185
|
#
|
|
1080
|
-
# BROKEN — a lane IS declared and its
|
|
1186
|
+
# BROKEN — a lane IS declared and its lane root folder is empty or is not a directory.
|
|
1081
1187
|
# Checked for EVERY declared lane whether or not the caller asked about it. A
|
|
1082
1188
|
# stack named against a path that is not there is a mistake in every phase, and
|
|
1083
1189
|
# letting it through means discovering it as an empty build or a generator
|
|
@@ -1106,7 +1212,7 @@ def cmd_verify(args):
|
|
|
1106
1212
|
issues = []
|
|
1107
1213
|
|
|
1108
1214
|
# An empty file and an absent one are the SAME state. The kit ships
|
|
1109
|
-
# stacks.json
|
|
1215
|
+
# stacks.json with an empty `lanes` array so a project can fill it in by hand, so
|
|
1110
1216
|
# the file being there says nothing about whether a lane was declared — only
|
|
1111
1217
|
# its contents do. Both cases report one issue naming which of the two it is.
|
|
1112
1218
|
if not lanes:
|
|
@@ -1130,13 +1236,13 @@ def cmd_verify(args):
|
|
|
1130
1236
|
root = lane.get("root", "")
|
|
1131
1237
|
if not root:
|
|
1132
1238
|
issues.append(
|
|
1133
|
-
"lane '%s' (%s, stack %s) declares no
|
|
1239
|
+
"lane '%s' (%s, stack %s) declares no lane root folder. A lane owns a "
|
|
1134
1240
|
"subtree; without one nothing can be built, run or written into it. "
|
|
1135
|
-
"Re-run `/appweaver select-stack` and give it a
|
|
1241
|
+
"Re-run `/appweaver select-stack` and give it a lane root folder."
|
|
1136
1242
|
% (lane_id, kind, lane.get("profile", "?")))
|
|
1137
1243
|
elif not args.allow_missing_root and not root_on_disk(repo, root):
|
|
1138
1244
|
issues.append(
|
|
1139
|
-
"lane '%s' (%s, stack %s) declares
|
|
1245
|
+
"lane '%s' (%s, stack %s) declares lane root folder '%s', which is not a "
|
|
1140
1246
|
"directory in this repository. Re-run `/appweaver select-stack` and "
|
|
1141
1247
|
"point the lane at the directory that holds its source."
|
|
1142
1248
|
% (lane_id, kind, lane.get("profile", "?"), root))
|
|
@@ -1144,7 +1250,7 @@ def cmd_verify(args):
|
|
|
1144
1250
|
# hand-edited file — a supported way to fill this in, and the one path that
|
|
1145
1251
|
# does not pass the writer. The invariant is worth a second check anywhere:
|
|
1146
1252
|
# two lanes sharing a subtree means two generators can write one file. A lane
|
|
1147
|
-
# owns its test path as well as its
|
|
1253
|
+
# owns its test path as well as its lane root folder, so both are compared.
|
|
1148
1254
|
for i, first in enumerate(lanes):
|
|
1149
1255
|
for second in lanes[i + 1:]:
|
|
1150
1256
|
clash = next(((fl, fp, sl, sp)
|
|
@@ -1154,7 +1260,7 @@ def cmd_verify(args):
|
|
|
1154
1260
|
if clash:
|
|
1155
1261
|
issues.append(
|
|
1156
1262
|
"lanes '%s' and '%s' claim overlapping subtrees: %s '%s' and %s '%s'. "
|
|
1157
|
-
"A lane owns its
|
|
1263
|
+
"A lane owns its lane root folder and its test path, and they must be "
|
|
1158
1264
|
"disjoint from every other lane's, or two generators can write the "
|
|
1159
1265
|
"same file. Give each lane paths outside the other's."
|
|
1160
1266
|
% (first.get("id", "?"), second.get("id", "?"),
|
|
@@ -1191,7 +1297,7 @@ def cmd_verify(args):
|
|
|
1191
1297
|
# ────────────────────────────── run-phase ──────────────────────────────────
|
|
1192
1298
|
# Execute a phase's project-supplied command for EVERY declared lane.
|
|
1193
1299
|
#
|
|
1194
|
-
# This is the payoff of declaring a
|
|
1300
|
+
# This is the payoff of declaring a lane root folder. Without a declaration, `build`
|
|
1195
1301
|
# sniffs: it looks for a .sln, then for a package.json, and takes the first hit.
|
|
1196
1302
|
# On a single-lane repo that is right. On a two-lane repo it is wrong in the worst
|
|
1197
1303
|
# way available — it builds the backend, reports green, and never mentions that the
|
|
@@ -1256,21 +1362,21 @@ def cmd_run_phase(args):
|
|
|
1256
1362
|
testlog = _testlog()
|
|
1257
1363
|
ok, ran = True, False
|
|
1258
1364
|
for lane in lanes:
|
|
1259
|
-
cmd = lane_command(lane, args.phase)
|
|
1365
|
+
cmd = lane_command(lane, args.phase, repo)
|
|
1260
1366
|
lane_id, root = lane.get("id", "?"), lane.get("root", "")
|
|
1261
1367
|
if not cmd:
|
|
1262
1368
|
results.append({"lane": lane_id, "root": root, "cmd": "",
|
|
1263
1369
|
"ran": False, "ok": True, "skipped": "no %s command" % args.phase})
|
|
1264
1370
|
continue
|
|
1265
|
-
# The
|
|
1371
|
+
# The lane root folder has to be there before a command is run against it.
|
|
1266
1372
|
# Reported per lane rather than raised, so a two-lane run still reports
|
|
1267
1373
|
# what the other lane did.
|
|
1268
1374
|
if not root_on_disk(repo, root):
|
|
1269
1375
|
ok = False
|
|
1270
1376
|
results.append({"lane": lane_id, "root": root, "cmd": cmd, "ran": False, "ok": False,
|
|
1271
|
-
"error": "
|
|
1272
|
-
tails.append("[%s]
|
|
1273
|
-
excerpts.append("[%s]
|
|
1377
|
+
"error": "lane root folder '%s' is not a directory" % root})
|
|
1378
|
+
tails.append("[%s] lane root folder '%s' is not a directory" % (lane_id, root))
|
|
1379
|
+
excerpts.append("[%s] lane root folder '%s' is not a directory — nothing ran"
|
|
1274
1380
|
% (lane_id, root))
|
|
1275
1381
|
failing.append(lane_id)
|
|
1276
1382
|
continue
|
|
@@ -1456,7 +1562,7 @@ def main():
|
|
|
1456
1562
|
p.add_argument("--kind", choices=["backend", "frontend"])
|
|
1457
1563
|
p.add_argument("--id", help="lane id (default: service for backend, web for frontend)")
|
|
1458
1564
|
p.add_argument("--root", "--source-path", dest="root",
|
|
1459
|
-
help="the
|
|
1565
|
+
help="the LANE ROOT FOLDER — the subtree it owns, overriding the "
|
|
1460
1566
|
"profile's default root. The two spellings are one field")
|
|
1461
1567
|
p.add_argument("--run-from", dest="run_from", choices=["repo", "root"],
|
|
1462
1568
|
help="where this lane's build/test/run commands execute: 'repo' (default) "
|
|
@@ -1466,7 +1572,7 @@ def main():
|
|
|
1466
1572
|
"subtree (a Maven wrapper at backend/<app>/mvnw). Defaults to the "
|
|
1467
1573
|
"profile's own setting")
|
|
1468
1574
|
p.add_argument("--require-existing-root", dest="require_existing_root", action="store_true",
|
|
1469
|
-
help="refuse a
|
|
1575
|
+
help="refuse a lane root folder that is not a directory. For a caller about "
|
|
1470
1576
|
"to READ the subtree (onboard); greenfield declares a path that "
|
|
1471
1577
|
"implement has not created yet, and leaves this off")
|
|
1472
1578
|
p.add_argument("--stack", default="",
|
|
@@ -1498,7 +1604,7 @@ def main():
|
|
|
1498
1604
|
help="a lane kind this phase cannot run without; repeatable. Omit to "
|
|
1499
1605
|
"check only that whatever IS declared is sound")
|
|
1500
1606
|
p.add_argument("--allow-missing-root", dest="allow_missing_root", action="store_true",
|
|
1501
|
-
help="do not require the
|
|
1607
|
+
help="do not require the lane root folders to exist on disk yet — the "
|
|
1502
1608
|
"greenfield state, where select-stack declares a path implement "
|
|
1503
1609
|
"will create")
|
|
1504
1610
|
p.add_argument("--command", default="",
|
|
@@ -4,11 +4,24 @@
|
|
|
4
4
|
"title": "ASP.NET Core Web API",
|
|
5
5
|
"language": "C#",
|
|
6
6
|
"ecosystem": "dotnet",
|
|
7
|
-
"root": "
|
|
8
|
-
"test_root": "tests/",
|
|
7
|
+
"root": "backend/",
|
|
8
|
+
"test_root": "backend/tests/",
|
|
9
|
+
"run_from": "root",
|
|
10
|
+
"conventional_root": "src/",
|
|
11
|
+
"conventional_test_root": "tests/",
|
|
12
|
+
"build_files": [
|
|
13
|
+
"*.sln",
|
|
14
|
+
"*.slnx",
|
|
15
|
+
"*.csproj"
|
|
16
|
+
],
|
|
9
17
|
"build": "dotnet build",
|
|
10
18
|
"test": "dotnet test",
|
|
11
|
-
"run": "dotnet run --project {{
|
|
19
|
+
"run": "dotnet run --project {{host}}",
|
|
20
|
+
"host_patterns": [
|
|
21
|
+
"**/*.AppHost.csproj",
|
|
22
|
+
"**/*.Api.csproj",
|
|
23
|
+
"**/*.Service.csproj"
|
|
24
|
+
],
|
|
12
25
|
"detect": [
|
|
13
26
|
"*.sln",
|
|
14
27
|
"src/**/*.csproj"
|