verdog-cli 0.1.2__tar.gz → 0.1.4__tar.gz

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 (32) hide show
  1. {verdog_cli-0.1.2/verdog_cli.egg-info → verdog_cli-0.1.4}/PKG-INFO +2 -2
  2. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/pyproject.toml +2 -2
  3. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/tests/test_backend_handoff.py +3 -0
  4. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/tests/test_catalogue_cli.py +26 -4
  5. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/tests/test_sync.py +99 -6
  6. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/local.py +40 -33
  7. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/main.py +2 -1
  8. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/manage.py +78 -55
  9. {verdog_cli-0.1.2 → verdog_cli-0.1.4/verdog_cli.egg-info}/PKG-INFO +2 -2
  10. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli.egg-info/requires.txt +1 -1
  11. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/LICENSE +0 -0
  12. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/MANIFEST.in +0 -0
  13. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/README.md +0 -0
  14. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/setup.cfg +0 -0
  15. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/tests/conftest.py +0 -0
  16. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/tests/test_check_receipt.py +0 -0
  17. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/tests/test_install_process.py +0 -0
  18. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/tests/test_runs.py +0 -0
  19. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/tests/test_session.py +0 -0
  20. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/__init__.py +0 -0
  21. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/api.py +0 -0
  22. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/catalogue.py +0 -0
  23. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/py.typed +0 -0
  24. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/requirements.py +0 -0
  25. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/runner.py +0 -0
  26. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/runs.py +0 -0
  27. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/session.py +0 -0
  28. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli/sync.py +0 -0
  29. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli.egg-info/SOURCES.txt +0 -0
  30. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli.egg-info/dependency_links.txt +0 -0
  31. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli.egg-info/entry_points.txt +0 -0
  32. {verdog_cli-0.1.2 → verdog_cli-0.1.4}/verdog_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: verdog-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: The Verdog command-line client.
5
5
  License-Expression: AGPL-3.0-only
6
6
  Project-URL: Repository, https://github.com/verdog-ai/verdog-cli
@@ -8,7 +8,7 @@ Project-URL: Documentation, https://drexlerd.github.io/verdog-website/
8
8
  Requires-Python: >=3.12
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: verdog-runtime<0.2,>=0.1.4
11
+ Requires-Dist: verdog-runtime<0.2,>=0.1.6
12
12
  Requires-Dist: packaging>=23.2
13
13
  Requires-Dist: tabulate>=0.10
14
14
  Requires-Dist: ty==0.0.72
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "verdog-cli"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "The Verdog command-line client."
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-only"
11
11
  license-files = ["LICENSE"]
12
12
  requires-python = ">=3.12"
13
- dependencies = ["verdog-runtime>=0.1.4,<0.2", "packaging>=23.2", "tabulate>=0.10", "ty==0.0.72"]
13
+ dependencies = ["verdog-runtime>=0.1.6,<0.2", "packaging>=23.2", "tabulate>=0.10", "ty==0.0.72"]
14
14
 
15
15
  [project.scripts]
16
16
  verdog = "verdog_cli.main:main"
@@ -304,6 +304,9 @@ def test_new_projects_persist_the_selected_backend(
304
304
  assert received == (
305
305
  [f"{expected}/api/v1/check"] if command == "init" else []
306
306
  )
307
+ if command == "init":
308
+ project = json.loads((destination / "project.json").read_text("utf-8"))
309
+ assert (project["workflows"], project["subroutines"]) == ([], [])
307
310
 
308
311
 
309
312
  @pytest.mark.parametrize("selection", ["flag", "environment"])
@@ -72,7 +72,7 @@ def test_describe_json_is_canonical_and_read_only(
72
72
  captured = capsys.readouterr()
73
73
  assert captured.err == ""
74
74
  assert json.loads(captured.out) == {
75
- "schema_version": 32,
75
+ "schema_version": 33,
76
76
  "package": "test.project",
77
77
  "workflow_id": "main",
78
78
  "display_name": "Research workflow",
@@ -114,7 +114,7 @@ def test_describe_json_is_canonical_and_read_only(
114
114
  "closure": [],
115
115
  "environment": {
116
116
  "python": ">=3.12",
117
- "schema_version": 32,
117
+ "schema_version": 33,
118
118
  "requirements": ["requests[security]>=2", "zeta>=2"],
119
119
  },
120
120
  }
@@ -171,7 +171,7 @@ def test_catalogue_client_encodes_filters_and_publication_environment(
171
171
  {"name": "Tools"},
172
172
  environment={
173
173
  "python": ">=3.12",
174
- "schema_version": 32,
174
+ "schema_version": 33,
175
175
  "requirements": ["httpx>=0.27"],
176
176
  },
177
177
  )
@@ -197,7 +197,7 @@ def test_catalogue_client_encodes_filters_and_publication_environment(
197
197
  "preview": {"name": "Tools"},
198
198
  "environment": {
199
199
  "python": ">=3.12",
200
- "schema_version": 32,
200
+ "schema_version": 33,
201
201
  "requirements": ["httpx>=0.27"],
202
202
  },
203
203
  },
@@ -704,3 +704,25 @@ def test_retract_reports_the_entry_id_after_an_empty_204_response(
704
704
  assert captured.out.startswith(f"Retracted catalogue entry {entry_id}.\n")
705
705
  assert "@" not in captured.out
706
706
  assert retracted == [entry_id]
707
+
708
+
709
+ @pytest.mark.parametrize(
710
+ ("roots", "requested", "expected"),
711
+ [
712
+ (["only"], None, "only"),
713
+ (["left", "main"], None, "main"),
714
+ (["left", "right"], "right", "right"),
715
+ (["left", "right"], None, "choose a root subroutine explicitly"),
716
+ ],
717
+ )
718
+ def test_import_into_defaults_to_the_only_root_subroutine_or_main(
719
+ roots: list[str], requested: str | None, expected: str
720
+ ) -> None:
721
+ from verdog_cli import manage
722
+
723
+ project = {"subroutines": [{"id": root} for root in roots]}
724
+ if expected.startswith("choose"):
725
+ with pytest.raises(WorkspaceError, match="left, right"):
726
+ manage._import_parent(project, requested) # pyright: ignore[reportPrivateUsage]
727
+ return
728
+ assert manage._import_parent(project, requested) == expected # pyright: ignore[reportPrivateUsage]
@@ -20,6 +20,7 @@ from verdog_cli.local import (
20
20
  Clone,
21
21
  LocalDefinition,
22
22
  WorkspaceError,
23
+ default_root,
23
24
  )
24
25
  from verdog_cli.requirements import parse_requirements
25
26
  from verdog_cli.sync import (
@@ -100,8 +101,8 @@ def _manifest(
100
101
  {
101
102
  "schema_version": SCHEMA_VERSION,
102
103
  "package": package,
103
- "workflow": _workflow(cast(str, root_subroutine["id"])),
104
- "subroutine": root_subroutine,
104
+ "workflows": [_workflow(cast(str, root_subroutine["id"]))],
105
+ "subroutines": [root_subroutine],
105
106
  "externals": [] if externals is None else externals,
106
107
  "sources": [],
107
108
  }
@@ -628,7 +629,7 @@ def test_environment_ids_cannot_escape_the_generated_root(
628
629
  ) -> None:
629
630
  clone = _clone(tmp_path, [])
630
631
  project = clone.project
631
- project["workflow"]["subroutine"] = "../outside"
632
+ project["workflows"][0]["subroutine"] = "../outside"
632
633
  (clone.root / "project.json").write_text(
633
634
  json.dumps(project), encoding="utf-8"
634
635
  )
@@ -648,7 +649,7 @@ def test_local_definition_ids_use_the_language_identifier_grammar(
648
649
  ) -> None:
649
650
  clone = _clone(tmp_path, [])
650
651
  project = clone.project
651
- project["subroutine"]["subroutines"] = [_subroutine(identifier)]
652
+ project["subroutines"][0]["subroutines"] = [_subroutine(identifier)]
652
653
  (clone.root / "project.json").write_text(
653
654
  json.dumps(project), encoding="utf-8"
654
655
  )
@@ -777,8 +778,8 @@ def test_sync_visits_every_workflow_in_each_nested_checkout(
777
778
 
778
779
  # Provision local workflow envelopes independently, also below pins.
779
780
  left_project = left_clone.project
780
- left_project["subroutine"]["workflows"] = [_workflow("main__other")]
781
- left_project["subroutine"]["subroutines"] = [_subroutine("other")]
781
+ left_project["subroutines"][0]["workflows"] = [_workflow("main__other")]
782
+ left_project["subroutines"][0]["subroutines"] = [_subroutine("other")]
782
783
  (left / "project.json").write_text(
783
784
  json.dumps(left_project), encoding="utf-8"
784
785
  )
@@ -1521,3 +1522,95 @@ def test_reachable_local_workflow_preflight_handles_recursive_calls(
1521
1522
  monkeypatch.setattr("verdog_cli.sync.require_current_environment", require)
1522
1523
  require_current_workflow_environments(root, _root_definition(root))
1523
1524
  assert checked == ["main", "main__child"]
1525
+
1526
+
1527
+ def _roots(root: Path, project: dict[str, Any]) -> Clone:
1528
+ root.mkdir(parents=True, exist_ok=True)
1529
+ (root / "project.json").write_text(
1530
+ json.dumps(
1531
+ {"schema_version": SCHEMA_VERSION, "package": "ada.roots"} | project
1532
+ ),
1533
+ encoding="utf-8",
1534
+ )
1535
+ return Clone(root, "http://unused", None)
1536
+
1537
+
1538
+ @pytest.mark.parametrize(
1539
+ ("ids", "expected"),
1540
+ [(["one"], "one"), (["alpha", "main", "beta"], "main")],
1541
+ )
1542
+ def test_default_root_is_the_only_one_or_main(
1543
+ ids: list[str], expected: str
1544
+ ) -> None:
1545
+ assert default_root("workflow", ids) == expected
1546
+
1547
+
1548
+ @pytest.mark.parametrize(
1549
+ ("ids", "listed"),
1550
+ [(["alpha", "beta"], "alpha, beta"), ([], "none declared")],
1551
+ )
1552
+ def test_default_root_otherwise_asks_for_a_choice(
1553
+ ids: list[str], listed: str
1554
+ ) -> None:
1555
+ with pytest.raises(
1556
+ WorkspaceError, match=f"choose a root subroutine explicitly: {listed}"
1557
+ ):
1558
+ default_root("subroutine", ids)
1559
+
1560
+
1561
+ def test_a_v32_manifest_reads_as_one_root_workflow_and_subroutine(
1562
+ tmp_path: Path,
1563
+ ) -> None:
1564
+ clone = _roots(
1565
+ tmp_path,
1566
+ {
1567
+ "schema_version": 32,
1568
+ "workflow": _workflow("main"),
1569
+ "subroutine": _subroutine("main"),
1570
+ },
1571
+ )
1572
+ assert clone.project["schema_version"] == SCHEMA_VERSION
1573
+ assert [(item.kind, item.module) for item in clone.local_definitions()] == [
1574
+ ("workflow", "ada.roots.workflows.main"),
1575
+ ("subroutine", "ada.roots.subroutines.main"),
1576
+ ]
1577
+ assert clone.workflow_definition().local_id == "main"
1578
+
1579
+
1580
+ def test_empty_and_multi_root_projects_list_their_definitions(
1581
+ tmp_path: Path,
1582
+ ) -> None:
1583
+ empty = _roots(tmp_path / "empty", {"workflows": [], "subroutines": []})
1584
+ assert empty.local_definitions() == ()
1585
+ with pytest.raises(WorkspaceError, match="none declared"):
1586
+ empty.workflow_definition()
1587
+
1588
+ clone = _roots(
1589
+ tmp_path / "two",
1590
+ {
1591
+ "workflows": [_workflow("left"), _workflow("right")],
1592
+ "subroutines": [
1593
+ _subroutine("left", subroutines=[_subroutine("inner")]),
1594
+ _subroutine("right"),
1595
+ ],
1596
+ },
1597
+ )
1598
+ assert [
1599
+ (item.kind, item.local_id, item.module)
1600
+ for item in clone.local_definitions()
1601
+ ] == [
1602
+ ("workflow", "left", "ada.roots.workflows.left"),
1603
+ ("workflow", "right", "ada.roots.workflows.right"),
1604
+ ("subroutine", "left", "ada.roots.subroutines.left"),
1605
+ (
1606
+ "subroutine",
1607
+ "left__inner",
1608
+ "ada.roots.subroutines.left.subroutines.inner",
1609
+ ),
1610
+ ("subroutine", "right", "ada.roots.subroutines.right"),
1611
+ ]
1612
+ assert clone.workflow_definition("right").local_id == "right"
1613
+ with pytest.raises(
1614
+ WorkspaceError, match="choose a root workflow explicitly: left, right"
1615
+ ):
1616
+ runner.run(clone)
@@ -36,7 +36,7 @@ EXTERNAL_ROOT = "external"
36
36
  Shared by dependency paths, type-check exclusions, and drop cleanup.
37
37
  """
38
38
 
39
- SCHEMA_VERSION = 32
39
+ SCHEMA_VERSION = 33
40
40
 
41
41
  _DEFINITION_SEPARATOR = "__"
42
42
 
@@ -51,6 +51,17 @@ def definition_leaf(identifier: str, /) -> str:
51
51
  return identifier.rsplit(_DEFINITION_SEPARATOR, 1)[-1]
52
52
 
53
53
 
54
+ def default_root(kind: str, ids: list[str]) -> str:
55
+ """The root definition used when none is named: the only one, or `main`."""
56
+ if len(ids) == 1:
57
+ return ids[0]
58
+ if "main" in ids:
59
+ return "main"
60
+ raise WorkspaceError(
61
+ f"choose a root {kind} explicitly: {', '.join(ids) or 'none declared'}"
62
+ )
63
+
64
+
54
65
  def package_problem(package: str) -> str | None:
55
66
  """Why a canonical `<space>.<name>` package is invalid, if it is."""
56
67
  parts = package.split(".")
@@ -308,11 +319,19 @@ class Clone:
308
319
  raise WorkspaceError("project.json is not an object")
309
320
  project = cast(dict[str, Any], decoded)
310
321
  version = project.get("schema_version")
311
- if version != SCHEMA_VERSION:
322
+ if version == 32:
323
+ # ponytail: the compiler migrates the file; reading v32 only needs
324
+ # its single root workflow and subroutine lifted into lists.
325
+ workflow = project.pop("workflow", None)
326
+ subroutine = project.pop("subroutine", None)
327
+ project["schema_version"] = SCHEMA_VERSION
328
+ project["workflows"] = [] if workflow is None else [workflow]
329
+ project["subroutines"] = [] if subroutine is None else [subroutine]
330
+ elif version != SCHEMA_VERSION:
312
331
  raise WorkspaceError(
313
332
  f"project schema v"
314
333
  f"{version} is unsupported; this CLI requires schema v"
315
- f"{SCHEMA_VERSION}"
334
+ f"{SCHEMA_VERSION} (or v32)"
316
335
  )
317
336
  return project
318
337
 
@@ -563,18 +582,12 @@ class Clone:
563
582
  """Return the declared workflow envelopes and subroutine graphs."""
564
583
  project = self.project
565
584
  package = project.get("package")
566
- root_workflow = project.get("workflow")
567
- root_subroutine = project.get("subroutine")
568
585
  if (
569
586
  not isinstance(package, str)
570
587
  or not package
571
588
  or package_problem(package) is not None
572
- or not isinstance(root_workflow, dict)
573
- or not isinstance(root_subroutine, dict)
574
589
  ):
575
- raise WorkspaceError(
576
- "project.json has no root workflow and subroutine"
577
- )
590
+ raise WorkspaceError("project.json has no valid package")
578
591
 
579
592
  definitions: list[LocalDefinition] = []
580
593
  identifiers: set[tuple[Literal["workflow", "subroutine"], str]] = set()
@@ -622,13 +635,18 @@ class Clone:
622
635
  raise WorkspaceError("a local subroutine has no id")
623
636
  identifier = definition_child(owner, leaf)
624
637
  add(raw, module, "subroutine", identifier)
638
+ visit_children(raw, module, identifier)
639
+
640
+ def visit_children(
641
+ raw: dict[str, Any], module: str, owner: str | None
642
+ ) -> None:
625
643
  for child in objects(raw.get("workflows"), "workflows"):
626
644
  if "external" in child:
627
645
  continue
628
646
  target = child.get("subroutine")
629
647
  if not isinstance(target, str) or not target:
630
648
  raise WorkspaceError("a local workflow has no subroutine")
631
- child_id = definition_child(identifier, definition_leaf(target))
649
+ child_id = definition_child(owner, definition_leaf(target))
632
650
  add(
633
651
  child,
634
652
  f"{module}.workflows.{definition_leaf(target)}",
@@ -642,26 +660,10 @@ class Clone:
642
660
  visit_subroutine(
643
661
  child,
644
662
  f"{module}.subroutines.{child_id}",
645
- identifier,
663
+ owner,
646
664
  )
647
665
 
648
- workflow_body = cast(dict[str, Any], root_workflow)
649
- workflow_id = workflow_body.get("subroutine")
650
- subroutine_body = cast(dict[str, Any], root_subroutine)
651
- subroutine_id = subroutine_body.get("id")
652
- if not isinstance(subroutine_id, str) or not subroutine_id:
653
- raise WorkspaceError("the root subroutine has no id")
654
- add(
655
- workflow_body,
656
- f"{package}.workflows.{workflow_id}",
657
- "workflow",
658
- workflow_id,
659
- )
660
- visit_subroutine(
661
- subroutine_body,
662
- f"{package}.subroutines.{subroutine_id}",
663
- None,
664
- )
666
+ visit_children(project, package, None)
665
667
  return tuple(definitions)
666
668
 
667
669
  def workflow_definitions(self) -> tuple[LocalDefinition, ...]:
@@ -675,12 +677,17 @@ class Clone:
675
677
  def workflow_definition(
676
678
  self, identifier: str | None = None, /
677
679
  ) -> LocalDefinition:
678
- """Select a workflow by local id, defaulting to the first envelope."""
680
+ """Select a workflow by local id, defaulting per `default_root`."""
679
681
  workflows = self.workflow_definitions()
680
- if not workflows:
681
- raise WorkspaceError("project.json has no workflow")
682
682
  if identifier is None:
683
- return workflows[0]
683
+ identifier = default_root(
684
+ "workflow",
685
+ [
686
+ item.local_id
687
+ for item in workflows
688
+ if _DEFINITION_SEPARATOR not in item.local_id
689
+ ],
690
+ )
684
691
  found = next(
685
692
  (item for item in workflows if item.local_id == identifier), None
686
693
  )
@@ -470,7 +470,8 @@ def build_parser() -> argparse.ArgumentParser:
470
470
  "workflow",
471
471
  nargs="?",
472
472
  metavar="WORKFLOW",
473
- help="project-local workflow path (the root workflow by default)",
473
+ help="project-local workflow path (default: the only root workflow, "
474
+ "else main)",
474
475
  )
475
476
  execute.add_argument(
476
477
  "--output-dir",
@@ -37,9 +37,6 @@ def _objects(value: Any) -> list[dict[str, Any]]:
37
37
  def _scoped_subroutine_definitions(
38
38
  project: dict[str, Any],
39
39
  ) -> list[tuple[str, dict[str, Any]]]:
40
- root = _object(project.get("subroutine"))
41
- if not root:
42
- return []
43
40
  found: list[tuple[str, dict[str, Any]]] = []
44
41
 
45
42
  def visit(definition: dict[str, Any], owner: str | None) -> None:
@@ -50,7 +47,8 @@ def _scoped_subroutine_definitions(
50
47
  for child in _objects(definition.get("subroutines")):
51
48
  visit(child, identifier)
52
49
 
53
- visit(root, None)
50
+ for root in _objects(project.get("subroutines")):
51
+ visit(root, None)
54
52
  return found
55
53
 
56
54
 
@@ -208,11 +206,9 @@ def _clone(arguments: argparse.Namespace) -> int:
208
206
 
209
207
 
210
208
  ENTRY_WORKFLOW = "main"
211
- """The fixed root workflow/subroutine id.
209
+ """The root workflow/subroutine id of `blank_graph`'s one-workflow project.
212
210
 
213
- The service uses the same root id. Integration tests run this client's
214
- output
215
- through the compiler; the client itself depends only on the public runtime.
211
+ Backend integration tests and fixtures build on it; `init` starts empty.
216
212
  """
217
213
 
218
214
 
@@ -272,44 +268,52 @@ def blank_graph(package: str, name: str) -> dict[str, Any]:
272
268
  "schema_version": local.SCHEMA_VERSION,
273
269
  "package": package,
274
270
  "generated_from": "0" * 64,
275
- "workflow": {
276
- "subroutine": ENTRY_WORKFLOW,
277
- "profiles": [],
278
- "sessions": [],
279
- "profile_arguments": {},
280
- "session_arguments": {},
281
- },
282
- "subroutine": {
283
- "id": ENTRY_WORKFLOW,
284
- "name": name,
285
- "ports": {"enter": "enter", "exit": "exit", "failure": "failure"},
286
- "features": [],
287
- "profiles": [],
288
- "profile_parameters": [],
289
- "sessions": [],
290
- "session_parameters": [],
291
- "nodes": [
292
- port("enter", "Enter"),
293
- port("exit", "Exit"),
294
- port("failure", "Failure"),
295
- ],
296
- "edges": [
297
- {
298
- "id": "enter__exit",
299
- "name": "Pass through",
300
- "source": "enter",
301
- "target": "exit",
302
- "conditions": [],
303
- "effects": [],
304
- }
305
- ],
306
- "workflows": [],
307
- "subroutines": [],
308
- },
271
+ "workflows": [
272
+ {
273
+ "subroutine": ENTRY_WORKFLOW,
274
+ "profiles": [],
275
+ "sessions": [],
276
+ "profile_arguments": {},
277
+ "session_arguments": {},
278
+ }
279
+ ],
309
280
  "externals": [],
310
281
  "sources": [],
311
282
  "editor": {"layouts": {}},
312
283
  "extensions": {},
284
+ "subroutines": [
285
+ {
286
+ "id": ENTRY_WORKFLOW,
287
+ "name": name,
288
+ "ports": {
289
+ "enter": "enter",
290
+ "exit": "exit",
291
+ "failure": "failure",
292
+ },
293
+ "features": [],
294
+ "profiles": [],
295
+ "profile_parameters": [],
296
+ "sessions": [],
297
+ "session_parameters": [],
298
+ "nodes": [
299
+ port("enter", "Enter"),
300
+ port("exit", "Exit"),
301
+ port("failure", "Failure"),
302
+ ],
303
+ "edges": [
304
+ {
305
+ "id": "enter__exit",
306
+ "name": "Pass through",
307
+ "source": "enter",
308
+ "target": "exit",
309
+ "conditions": [],
310
+ "effects": [],
311
+ }
312
+ ],
313
+ "workflows": [],
314
+ "subroutines": [],
315
+ }
316
+ ],
313
317
  }
314
318
 
315
319
 
@@ -355,9 +359,14 @@ def _init(arguments: argparse.Namespace) -> int:
355
359
  )
356
360
 
357
361
  root.mkdir(parents=True, exist_ok=True)
362
+ # An empty project: root workflows and subroutines are added in the editor.
363
+ project: dict[str, Any] = {
364
+ **blank_graph(package, name),
365
+ "workflows": [],
366
+ "subroutines": [],
367
+ }
358
368
  (root / "project.json").write_text(
359
- json.dumps(blank_graph(package, name), indent=2, ensure_ascii=False)
360
- + "\n",
369
+ json.dumps(project, indent=2, ensure_ascii=False) + "\n",
361
370
  encoding="utf-8",
362
371
  )
363
372
  # Git, because every other verb finds the project by walking up to a `.git`.
@@ -372,13 +381,15 @@ def _init(arguments: argparse.Namespace) -> int:
372
381
  local.write_config(root, origin)
373
382
 
374
383
  print(f"Started {package} in {root}")
375
- print(f" workflow {ENTRY_WORKFLOW!r}: enter -> exit, with a failure port")
376
384
 
377
385
  from verdog_cli import main
378
386
 
379
387
  status = main.generate_clone(local.open_clone(root))
380
388
  if status == 0:
381
- print("Next: `verdog sync` to create the runtime environments.")
389
+ print(
390
+ "Next: add a subroutine and make it runnable, then run "
391
+ "`verdog sync` to create its runtime environment."
392
+ )
382
393
  return status
383
394
 
384
395
 
@@ -582,9 +593,9 @@ def publish_workflow(arguments: argparse.Namespace) -> int:
582
593
  )
583
594
  repository = f"{owner}/{name}"
584
595
  commit = clone_head(clone, repository)
585
- root_workflow = _object(clone.project.get("workflow"))
586
- workflow = str(arguments.workflow or root_workflow.get("subroutine", ""))
587
- descriptor = cli_catalogue.describe_workflow(clone, workflow)
596
+ descriptor = cli_catalogue.describe_workflow(
597
+ clone, arguments.workflow or None
598
+ )
588
599
  if arguments.private:
589
600
  print(
590
601
  "verdog: --private is deprecated; catalogue "
@@ -1065,11 +1076,19 @@ def _prepare_import(arguments: argparse.Namespace) -> _ImportPlan:
1065
1076
 
1066
1077
 
1067
1078
  def _import_parent(project: dict[str, Any], requested: object) -> str:
1068
- root = _object(project.get("subroutine"))
1069
- parent = str(requested or root.get("id", ""))
1070
1079
  known = {
1071
1080
  identifier for identifier, _ in _scoped_subroutine_definitions(project)
1072
1081
  }
1082
+ parent = str(
1083
+ requested
1084
+ or local.default_root(
1085
+ "subroutine",
1086
+ [
1087
+ str(root.get("id"))
1088
+ for root in _objects(project.get("subroutines"))
1089
+ ],
1090
+ )
1091
+ )
1073
1092
  if parent not in known:
1074
1093
  raise local.WorkspaceError(f"this project has no subroutine {parent}")
1075
1094
  return parent
@@ -2078,7 +2097,7 @@ def register(commands: Any) -> None:
2078
2097
  who.set_defaults(handler=_whoami)
2079
2098
 
2080
2099
  started = commands.add_parser("init", help="start a new project here")
2081
- started.add_argument("name", nargs="?", help="what the workflow is called")
2100
+ started.add_argument("name", nargs="?", help="what the project is called")
2082
2101
  started.add_argument(
2083
2102
  "directory", nargs="?", help="where to put it (default: here)"
2084
2103
  )
@@ -2145,7 +2164,8 @@ def register(commands: Any) -> None:
2145
2164
  describe.add_argument(
2146
2165
  "workflow",
2147
2166
  nargs="?",
2148
- help="project-local workflow path; defaults to the entry workflow",
2167
+ help="project-local workflow path (default: the only root workflow, "
2168
+ "else main)",
2149
2169
  )
2150
2170
  describe.add_argument(
2151
2171
  "--project",
@@ -2171,7 +2191,8 @@ def register(commands: Any) -> None:
2171
2191
  publish.add_argument(
2172
2192
  "workflow",
2173
2193
  nargs="?",
2174
- help="project-local workflow path; defaults to the entry workflow",
2194
+ help="project-local workflow path (default: the only root workflow, "
2195
+ "else main)",
2175
2196
  )
2176
2197
  publish.add_argument(
2177
2198
  "--private",
@@ -2207,7 +2228,9 @@ def register(commands: Any) -> None:
2207
2228
  "workflow", help="the published workflow path to call"
2208
2229
  )
2209
2230
  imported.add_argument(
2210
- "--into", help="the project-local subroutine path that declares it"
2231
+ "--into",
2232
+ help="the project-local subroutine path that declares it "
2233
+ "(default: the only root subroutine, else main)",
2211
2234
  )
2212
2235
  imported.add_argument(
2213
2236
  "--alias",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: verdog-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: The Verdog command-line client.
5
5
  License-Expression: AGPL-3.0-only
6
6
  Project-URL: Repository, https://github.com/verdog-ai/verdog-cli
@@ -8,7 +8,7 @@ Project-URL: Documentation, https://drexlerd.github.io/verdog-website/
8
8
  Requires-Python: >=3.12
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: verdog-runtime<0.2,>=0.1.4
11
+ Requires-Dist: verdog-runtime<0.2,>=0.1.6
12
12
  Requires-Dist: packaging>=23.2
13
13
  Requires-Dist: tabulate>=0.10
14
14
  Requires-Dist: ty==0.0.72
@@ -1,4 +1,4 @@
1
- verdog-runtime<0.2,>=0.1.4
1
+ verdog-runtime<0.2,>=0.1.6
2
2
  packaging>=23.2
3
3
  tabulate>=0.10
4
4
  ty==0.0.72
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes