diff --git a/cmd/hyard/cli/cli_integration_test.go b/cmd/hyard/cli/cli_integration_test.go index d899a9e..30ef02e 100644 --- a/cmd/hyard/cli/cli_integration_test.go +++ b/cmd/hyard/cli/cli_integration_test.go @@ -5106,6 +5106,114 @@ func TestHyardCloneGitLocatorPackageCoordinateRecordsProvenanceJSON(t *testing.T require.Equal(t, payload.Source.Commit, record.Template.TemplateCommit) } +func TestHyardClonePackageHandleCoordinateFromGitRegistry(t *testing.T) { + sourceRepo := seedHyardCloneHarnessTemplateSourceRepo(t) + sourceRemote := testutil.NewBareRemoteFromRepo(t, sourceRepo) + sourceCommit := sourceRepo.RevParse(t, "harness-template/workspace") + + registryRepo := testutil.NewRepo(t) + registryRepo.Run(t, "branch", "-m", "main") + registryRepo.WriteFile(t, "curated/index.yaml", ""+ + "schema_version: 1\n"+ + "curated:\n"+ + " workspace:\n"+ + " target: acme/workspace\n") + registryRepo.WriteFile(t, "packages/acme/index.yaml", fmt.Sprintf(""+ + "schema_version: 1\n"+ + "namespace: acme\n"+ + "packages:\n"+ + " workspace:\n"+ + " handle: acme/workspace\n"+ + " status: active\n"+ + " package:\n"+ + " type: harness\n"+ + " name: workspace\n"+ + " source:\n"+ + " repository: %q\n"+ + " dist_tags:\n"+ + " latest: \"0.1.0\"\n"+ + " versions:\n"+ + " \"0.1.0\":\n"+ + " locator:\n"+ + " kind: git\n"+ + " repository: %q\n"+ + " ref: harness-template/workspace\n"+ + " commit: %s\n", sourceRemote, sourceRemote, sourceCommit)) + registryRepo.AddAndCommit(t, "seed clone package registry") + registryRemote := testutil.NewBareRemoteFromRepo(t, registryRepo) + parentDir := t.TempDir() + + lockHyardProcessEnv(t) + t.Setenv("HYARD_CACHE_DIR", t.TempDir()) + + stdout, stderr, err := executeHyardCLIUnlocked( + t, + parentDir, + "clone", + "Workspace", + "--registry-source", + registryRemote, + "--json", + ) + require.NoError(t, err) + require.Empty(t, stderr) + + var payload struct { + HarnessRoot string `json:"harness_root"` + HarnessID string `json:"harness_id"` + Source struct { + Repo string `json:"repo"` + RequestedRef string `json:"requested_ref"` + ResolvedRef string `json:"resolved_ref"` + Commit string `json:"commit"` + PackageName string `json:"package_name"` + PackageCoordinate string `json:"package_coordinate"` + PackageLocatorKind string `json:"package_locator_kind"` + PackageLocator string `json:"package_locator"` + RegistryProvenance struct { + RequestedCoordinate string `json:"requested_coordinate"` + ResolvedCoordinate string `json:"resolved_coordinate"` + ResolvedVersion string `json:"resolved_version"` + RegistryRemote string `json:"registry_remote"` + RegistryRef string `json:"registry_ref"` + PackageType string `json:"package_type"` + PackageIdentity string `json:"package_identity"` + SourceRemote string `json:"source_remote"` + SourceRef string `json:"source_ref"` + SourceCommit string `json:"source_commit"` + } `json:"registry_provenance"` + } `json:"source"` + } + require.NoError(t, json.Unmarshal([]byte(stdout), &payload)) + require.Equal(t, gitpkg.ComparablePath(filepath.Join(parentDir, "workspace")), gitpkg.ComparablePath(payload.HarnessRoot)) + require.NotEmpty(t, payload.HarnessID) + require.Equal(t, gitpkg.ComparablePath(sourceRemote), gitpkg.ComparablePath(payload.Source.Repo)) + require.Equal(t, sourceCommit, payload.Source.RequestedRef) + require.Equal(t, sourceCommit, payload.Source.ResolvedRef) + require.Equal(t, sourceCommit, payload.Source.Commit) + require.Equal(t, "workspace", payload.Source.PackageName) + require.Equal(t, "workspace@latest", payload.Source.PackageCoordinate) + require.Equal(t, "git", payload.Source.PackageLocatorKind) + require.Contains(t, payload.Source.PackageLocator, sourceCommit) + require.Equal(t, "workspace@latest", payload.Source.RegistryProvenance.RequestedCoordinate) + require.Equal(t, "acme/workspace@0.1.0", payload.Source.RegistryProvenance.ResolvedCoordinate) + require.Equal(t, "0.1.0", payload.Source.RegistryProvenance.ResolvedVersion) + require.Equal(t, gitpkg.ComparablePath(registryRemote), gitpkg.ComparablePath(payload.Source.RegistryProvenance.RegistryRemote)) + require.Equal(t, "HEAD", payload.Source.RegistryProvenance.RegistryRef) + require.Equal(t, "harness", payload.Source.RegistryProvenance.PackageType) + require.Equal(t, "workspace", payload.Source.RegistryProvenance.PackageIdentity) + require.Equal(t, gitpkg.ComparablePath(sourceRemote), gitpkg.ComparablePath(payload.Source.RegistryProvenance.SourceRemote)) + require.Equal(t, "harness-template/workspace", payload.Source.RegistryProvenance.SourceRef) + require.Equal(t, sourceCommit, payload.Source.RegistryProvenance.SourceCommit) + + record, err := harnesspkg.LoadBundleRecord(payload.HarnessRoot, payload.HarnessID) + require.NoError(t, err) + require.NotNil(t, record.Registry) + require.Equal(t, "workspace@latest", record.Registry.RequestedCoordinate) + require.Equal(t, "acme/workspace@0.1.0", record.Registry.ResolvedCoordinate) + require.Equal(t, sourceCommit, record.Registry.SourceCommit) +} + func TestHyardCloneRejectsInvalidHarnessTemplateSourceWithoutCreatingRepo(t *testing.T) { t.Parallel() diff --git a/cmd/hyard/cli/clone.go b/cmd/hyard/cli/clone.go index 72395fa..b3bfa43 100644 --- a/cmd/hyard/cli/clone.go +++ b/cmd/hyard/cli/clone.go @@ -17,6 +17,7 @@ import ( gitpkg "github.com/zack-nova/harnessyard/cmd/orbit/cli/git" harnesspkg "github.com/zack-nova/harnessyard/cmd/orbit/cli/harness" "github.com/zack-nova/harnessyard/cmd/orbit/cli/ids" + "github.com/zack-nova/harnessyard/cmd/orbit/cli/registry" orbittemplate "github.com/zack-nova/harnessyard/cmd/orbit/cli/template" ) @@ -27,15 +28,32 @@ const hyardStartLauncherContextKey hyardContextKey = "hyard_start_launcher" const hyardViewRunInteractiveContextKey hyardContextKey = "hyard_view_run_interactive" type cloneSourceJSON struct { - Kind string `json:"kind"` - Repo string `json:"repo"` - RequestedRef string `json:"requested_ref,omitempty"` - ResolvedRef string `json:"resolved_ref"` - Commit string `json:"commit"` - PackageName string `json:"package_name,omitempty"` - PackageCoordinate string `json:"package_coordinate,omitempty"` - PackageLocatorKind string `json:"package_locator_kind,omitempty"` - PackageLocator string `json:"package_locator,omitempty"` + Kind string `json:"kind"` + Repo string `json:"repo"` + RequestedRef string `json:"requested_ref,omitempty"` + ResolvedRef string `json:"resolved_ref"` + Commit string `json:"commit"` + PackageName string `json:"package_name,omitempty"` + PackageCoordinate string `json:"package_coordinate,omitempty"` + PackageLocatorKind string `json:"package_locator_kind,omitempty"` + PackageLocator string `json:"package_locator,omitempty"` + RegistryProvenance *cloneRegistryProvenanceJSON `json:"registry_provenance,omitempty"` +} + +type cloneRegistryProvenanceJSON struct { + RequestedCoordinate string `json:"requested_coordinate"` + ResolvedCoordinate string `json:"resolved_coordinate"` + ResolvedVersion string `json:"resolved_version"` + RegistryRemote string `json:"registry_remote"` + RegistryRef string `json:"registry_ref"` + PackageType string `json:"package_type"` + PackageIdentity string `json:"package_identity"` + PackageStatus string `json:"package_status,omitempty"` + SourceRemote string `json:"source_remote"` + SourceRef string `json:"source_ref"` + SourceCommit string `json:"source_commit"` + CacheUsed bool `json:"cache_used"` + CacheStale bool `json:"cache_stale"` } type cloneNextActionJSON struct { @@ -79,12 +97,14 @@ func hyardViewRunInteractiveFromContext(ctx context.Context) bool { func newCloneCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "clone [repo-name]", - Short: "Bootstrap a new runtime repo from a harness template source", - Long: "Bootstrap a new runtime repo from a harness template source.\n" + + Use: "clone [repo-name]", + Short: "Bootstrap a new runtime repo from a harness package handle or template source", + Long: "Bootstrap a new runtime repo from a harness package handle or template source.\n" + "This command is a controlled compose of runtime create plus harness template install,\n" + "reusing the existing runtime bootstrap, install preview, provenance, and readiness helpers.", Example: "" + + " hyard clone product-lab\n" + + " hyard clone zack-nova/product-lab demo-runtime\n" + " hyard clone ../starter-template --ref harness-template/workspace\n" + " hyard clone https://example.com/acme/starter.git demo --ref harness-template/workspace\n" + " hyard clone git@github.com:acme/starter.git --path ../workspaces --ref harness-template/workspace\n", @@ -100,7 +120,19 @@ func newCloneCommand() *cobra.Command { return fmt.Errorf("read --ref flag: %w", err) } sourceMetadata := packageMetadata{} - if shouldParseHyardPackageCoordinateArg(sourceArg) { + defaultRepoName := "" + var registryProvenance *orbittemplate.InstallRegistryProvenance + if registry.LooksPackageHandleCoordinate(sourceArg) { + resolution, err := resolvePackageHandleCloneCoordinate(cmd, sourceArg) + if err != nil { + return err + } + sourceArg = resolution.SourceRemote + requestedRef = resolution.SourceCommit + sourceMetadata = packageMetadataFromRegistryResolution(resolution) + registryProvenance = installRegistryProvenanceFromRegistryResolution(resolution) + defaultRepoName = resolution.Coordinate.Name + } else if shouldParseHyardPackageCoordinateArg(sourceArg) { coordinate, err := parseHyardPackageCoordinate(sourceArg) if err != nil { return err @@ -141,6 +173,9 @@ func newCloneCommand() *cobra.Command { return err } } + if repoName == "" { + repoName = defaultRepoName + } if repoName == "" { repoName, err = inferCloneRepoName(candidate.RepoURL) if err != nil { @@ -171,6 +206,7 @@ func newCloneCommand() *cobra.Command { InstallSource: installSource, RequireResolvedBindings: true, Now: time.Now().UTC(), + Registry: registryProvenance, }) if err != nil { return fmt.Errorf("build harness template install preview: %w", err) @@ -203,6 +239,7 @@ func newCloneCommand() *cobra.Command { PackageCoordinate: sourceMetadata.coordinate, PackageLocatorKind: sourceMetadata.locatorKind, PackageLocator: sourceMetadata.locator, + RegistryProvenance: cloneRegistryProvenancePayload(registryProvenance), }, MemberIDs: source.MemberIDs(), MemberCount: len(source.MemberIDs()), @@ -240,11 +277,106 @@ func newCloneCommand() *cobra.Command { cmd.Flags().String("ref", "", "Install one explicit harness template branch from the source repository") cmd.Flags().String("path", "", "Create the new runtime repo under this parent directory") + cmd.Flags().String("registry-source", "", "Git remote or local path for Package Handle Coordinate registry source") + cmd.Flags().String("registry-ref", registry.DefaultRegistryRef, "Git ref to read from the Package Handle Coordinate registry source") + cmd.Flags().Bool("allow-yanked", false, "Allow cloning a yanked Harness Package handle from the registry") cmd.Flags().Bool("json", false, "Output machine-readable JSON") return cmd } +func resolvePackageHandleCloneCoordinate(cmd *cobra.Command, raw string) (registry.Resolution, error) { + coordinate, err := registry.ParsePackageHandleCoordinate(raw) + if err != nil { + return registry.Resolution{}, fmt.Errorf("parse package handle coordinate: %w", err) + } + if cmd.Flags().Changed("ref") { + return registry.Resolution{}, fmt.Errorf("package handle coordinate %s cannot be combined with --ref; registry versions resolve their source ref from catalog data", coordinate.String()) + } + + registrySource, err := packageRegistrySourceFromCommand(cmd) + if err != nil { + return registry.Resolution{}, err + } + cacheRoot, err := registry.DefaultCacheRoot() + if err != nil { + return registry.Resolution{}, fmt.Errorf("resolve registry cache root: %w", err) + } + scratchRoot, err := os.MkdirTemp("", "hyard-clone-registry-*") + if err != nil { + return registry.Resolution{}, fmt.Errorf("create clone registry scratch repo: %w", err) + } + defer os.RemoveAll(scratchRoot) + + if _, err := gitpkg.EnsureRepoRoot(cmd.Context(), scratchRoot); err != nil { + return registry.Resolution{}, fmt.Errorf("initialize clone registry scratch repo: %w", err) + } + + resolution, err := registry.ResolvePackageHandleCoordinate(cmd.Context(), registry.ResolveInput{ + RepoRoot: scratchRoot, + Coordinate: coordinate, + RegistrySource: registrySource, + CacheRoot: cacheRoot, + }) + if err != nil { + return registry.Resolution{}, fmt.Errorf("resolve package handle coordinate: %w", err) + } + allowYanked, err := cmd.Flags().GetBool("allow-yanked") + if err != nil { + return registry.Resolution{}, fmt.Errorf("read --allow-yanked flag: %w", err) + } + if err := registry.RequireInstallableResolution(resolution, registry.InstallGateOptions{AllowYanked: allowYanked}); err != nil { + return registry.Resolution{}, fmt.Errorf("check registry package status: %w", err) + } + if resolution.PackageType != ids.PackageTypeHarness { + return registry.Resolution{}, fmt.Errorf("hyard clone package handle %s resolved to %s package %q; use `hyard install %s` inside an existing runtime", resolution.Coordinate.String(), resolution.PackageType, resolution.PackageIdentity, resolution.Coordinate.String()) + } + + return resolution, nil +} + +func installRegistryProvenanceFromRegistryResolution(resolution registry.Resolution) *orbittemplate.InstallRegistryProvenance { + exactCoordinate := resolution.ExactCoordinate() + cacheStale := resolution.FromCache && !resolution.Coordinate.IsExactVersion() + return &orbittemplate.InstallRegistryProvenance{ + RequestedCoordinate: resolution.Coordinate.String(), + ResolvedCoordinate: exactCoordinate.String(), + ResolvedVersion: exactCoordinate.Version, + RegistryRemote: resolution.RegistryRemote, + RegistryRef: resolution.RegistryRef, + PackageType: resolution.PackageType, + PackageIdentity: resolution.PackageIdentity, + PackageStatus: string(resolution.EffectivePackageStatus()), + SourceRemote: resolution.SourceRemote, + SourceRef: resolution.SourceRef, + SourceCommit: resolution.SourceCommit, + CacheUsed: resolution.FromCache, + CacheStale: cacheStale, + } +} + +func cloneRegistryProvenancePayload(provenance *orbittemplate.InstallRegistryProvenance) *cloneRegistryProvenanceJSON { + if provenance == nil { + return nil + } + + return &cloneRegistryProvenanceJSON{ + RequestedCoordinate: provenance.RequestedCoordinate, + ResolvedCoordinate: provenance.ResolvedCoordinate, + ResolvedVersion: provenance.ResolvedVersion, + RegistryRemote: provenance.RegistryRemote, + RegistryRef: provenance.RegistryRef, + PackageType: provenance.PackageType, + PackageIdentity: provenance.PackageIdentity, + PackageStatus: provenance.PackageStatus, + SourceRemote: provenance.SourceRemote, + SourceRef: provenance.SourceRef, + SourceCommit: provenance.SourceCommit, + CacheUsed: provenance.CacheUsed, + CacheStale: provenance.CacheStale, + } +} + func cloneHarnessStartNextActions(harnessRoot string) []cloneNextActionJSON { return []cloneNextActionJSON{{ Kind: "harness_start", diff --git a/cmd/hyard/cli/install_integration_test.go b/cmd/hyard/cli/install_integration_test.go index 6d57195..8a34789 100644 --- a/cmd/hyard/cli/install_integration_test.go +++ b/cmd/hyard/cli/install_integration_test.go @@ -72,15 +72,20 @@ func TestHyardInstallExactPackageHandleCoordinateFromGitRegistry(t *testing.T) { "namespace: acme\n"+ "packages:\n"+ " docs:\n"+ + " handle: acme/docs\n"+ " status: active\n"+ + " package:\n"+ + " type: orbit\n"+ + " name: docs\n"+ + " source:\n"+ + " repository: %q\n"+ " versions:\n"+ " 0.1.0:\n"+ - " package_type: orbit\n"+ - " package_identity: docs\n"+ - " source:\n"+ - " remote: %q\n"+ + " locator:\n"+ + " kind: git\n"+ + " repository: %q\n"+ " ref: orbit-template/docs\n"+ - " commit: %s\n", sourceRemote, sourceCommit)) + " commit: %s\n", sourceRemote, sourceRemote, sourceCommit)) registryRepo.AddAndCommit(t, "seed package registry") registryRemote := testutil.NewBareRemoteFromRepo(t, registryRepo) @@ -572,23 +577,29 @@ func seedPackageHandleInstallFixture(t *testing.T, packageStatus string) package "schema_version: 1\n"+ "curated:\n"+ " docs:\n"+ - " target: acme/docs\n") + " target: acme/docs\n"+ + " status: active\n") registryRepo.WriteFile(t, "packages/acme/index.yaml", fmt.Sprintf(""+ "schema_version: 1\n"+ "namespace: acme\n"+ "packages:\n"+ " docs:\n"+ + " handle: acme/docs\n"+ " status: %s\n"+ + " package:\n"+ + " type: orbit\n"+ + " name: docs\n"+ + " source:\n"+ + " repository: %q\n"+ " dist_tags:\n"+ " latest: 0.1.0\n"+ " versions:\n"+ " 0.1.0:\n"+ - " package_type: orbit\n"+ - " package_identity: docs\n"+ - " source:\n"+ - " remote: %q\n"+ + " locator:\n"+ + " kind: git\n"+ + " repository: %q\n"+ " ref: orbit-template/docs\n"+ - " commit: %s\n", packageStatus, sourceRemote, sourceCommit)) + " commit: %s\n", packageStatus, sourceRemote, sourceRemote, sourceCommit)) registryRepo.AddAndCommit(t, "seed package registry") registryRemote := testutil.NewBareRemoteFromRepo(t, registryRepo) diff --git a/cmd/hyard/cli/root.go b/cmd/hyard/cli/root.go index 60812ee..ef9e7ea 100644 --- a/cmd/hyard/cli/root.go +++ b/cmd/hyard/cli/root.go @@ -29,7 +29,7 @@ func NewRootCommand() *cobra.Command { Example: "" + " hyard create runtime demo-repo\n" + " hyard create source ./research-source --orbit research\n" + - " hyard clone ../starter-template --ref harness-template/workspace\n" + + " hyard clone product-lab\n" + " hyard vars init docs --out .harness/vars.yaml\n" + " hyard install docs --bindings .harness/vars.yaml\n" + " hyard current\n" + diff --git a/cmd/orbit/cli/registry/resolver.go b/cmd/orbit/cli/registry/resolver.go index a20ae14..90bbd61 100644 --- a/cmd/orbit/cli/registry/resolver.go +++ b/cmd/orbit/cli/registry/resolver.go @@ -367,12 +367,15 @@ func resolvePackageEntryVersion(coordinate PackageHandleCoordinate, resolvedCoor if err != nil { return Resolution{}, fmt.Errorf("package handle %q: %w", coordinate.Handle(), err) } + if err := validatePackageEntryHandle(resolvedCoordinate, entry); err != nil { + return Resolution{}, fmt.Errorf("package handle %q: %w", coordinate.Handle(), err) + } version, ok := versionEntryForExactVersion(entry.Versions, resolvedCoordinate.Version) if !ok { return Resolution{}, fmt.Errorf("package handle %q is not registered", resolvedCoordinate.String()) } - resolution, err := resolutionFromVersionEntry(resolvedCoordinate, version) + resolution, err := resolutionFromVersionEntry(resolvedCoordinate, entry, version) if err != nil { return Resolution{}, err } @@ -386,6 +389,25 @@ func resolvePackageEntryVersion(coordinate PackageHandleCoordinate, resolvedCoor return resolution, nil } +func validatePackageEntryHandle(coordinate PackageHandleCoordinate, entry packageEntry) error { + rawHandle := strings.ToLower(strings.TrimSpace(entry.Handle)) + if rawHandle == "" { + return nil + } + handleCoordinate, err := ParsePackageHandleCoordinate(rawHandle) + if err != nil { + return fmt.Errorf("registry package handle field: %w", err) + } + if handleCoordinate.Namespace == "" { + return fmt.Errorf("registry package handle field must be namespaced") + } + if handleCoordinate.Handle() != coordinate.Handle() { + return fmt.Errorf("registry package handle field must be %q, got %q", coordinate.Handle(), handleCoordinate.Handle()) + } + + return nil +} + func normalizePackageStatus(raw string) (PackageStatus, error) { status := PackageStatus(strings.ToLower(strings.TrimSpace(raw))) switch status { @@ -414,28 +436,27 @@ type curatedEntry struct { } type packageEntry struct { + Handle string `yaml:"handle"` Status string `yaml:"status"` + Package packageDescriptorEntry `yaml:"package"` DistTags map[string]string `yaml:"dist_tags"` Versions map[string]versionEntry `yaml:"versions"` } -type versionEntry struct { - PackageType string `yaml:"package_type"` - PackageIdentity string `yaml:"package_identity"` - Source versionSourceEntry `yaml:"source"` +type packageDescriptorEntry struct { + Type string `yaml:"type"` + Name string `yaml:"name"` } -type versionSourceEntry struct { - Remote string `yaml:"remote"` - Ref string `yaml:"ref"` - Commit string `yaml:"commit"` - Git *versionSourceGitEntry `yaml:"git"` +type versionEntry struct { + Locator versionLocatorEntry `yaml:"locator"` } -type versionSourceGitEntry struct { - Remote string `yaml:"remote"` - Ref string `yaml:"ref"` - Commit string `yaml:"commit"` +type versionLocatorEntry struct { + Kind string `yaml:"kind"` + Repository string `yaml:"repository"` + Ref string `yaml:"ref"` + Commit string `yaml:"commit"` } func packageEntryForName(packages map[string]packageEntry, name string) (packageEntry, bool) { @@ -484,31 +505,34 @@ func distTagVersion(distTags map[string]string, tag string) (string, bool) { return "", false } -func resolutionFromVersionEntry(coordinate PackageHandleCoordinate, version versionEntry) (Resolution, error) { - packageType := strings.ToLower(strings.TrimSpace(version.PackageType)) +func resolutionFromVersionEntry(coordinate PackageHandleCoordinate, entry packageEntry, version versionEntry) (Resolution, error) { + packageType := strings.ToLower(strings.TrimSpace(entry.Package.Type)) switch packageType { case ids.PackageTypeOrbit, ids.PackageTypeHarness: default: - return Resolution{}, fmt.Errorf("registry version %s package_type must be %q or %q", coordinate.String(), ids.PackageTypeOrbit, ids.PackageTypeHarness) + return Resolution{}, fmt.Errorf("registry package %s package.type must be %q or %q", coordinate.Handle(), ids.PackageTypeOrbit, ids.PackageTypeHarness) } - packageIdentity := strings.ToLower(strings.TrimSpace(version.PackageIdentity)) + packageIdentity := strings.ToLower(strings.TrimSpace(entry.Package.Name)) if _, err := ids.NewPackageIdentity(packageType, packageIdentity, coordinate.Version); err != nil { - return Resolution{}, fmt.Errorf("registry version %s package_identity: %w", coordinate.String(), err) + return Resolution{}, fmt.Errorf("registry package %s package.name: %w", coordinate.Handle(), err) } - sourceRemote, sourceRef, sourceCommit := version.Source.values() + sourceRemote, sourceRef, sourceCommit, err := version.Locator.values(coordinate) + if err != nil { + return Resolution{}, err + } if sourceRemote == "" { - return Resolution{}, fmt.Errorf("registry version %s source.remote must be present", coordinate.String()) + return Resolution{}, fmt.Errorf("registry version %s locator.repository must be present", coordinate.String()) } if sourceRef == "" { - return Resolution{}, fmt.Errorf("registry version %s source.ref must be present", coordinate.String()) + return Resolution{}, fmt.Errorf("registry version %s locator.ref must be present", coordinate.String()) } if sourceCommit == "" { - return Resolution{}, fmt.Errorf("registry version %s source.commit must be present", coordinate.String()) + return Resolution{}, fmt.Errorf("registry version %s locator.commit must be present", coordinate.String()) } if !commitPattern.MatchString(sourceCommit) { - return Resolution{}, fmt.Errorf("registry version %s source.commit must be a full Git commit SHA", coordinate.String()) + return Resolution{}, fmt.Errorf("registry version %s locator.commit must be a full Git commit SHA", coordinate.String()) } return Resolution{ @@ -522,23 +546,16 @@ func resolutionFromVersionEntry(coordinate PackageHandleCoordinate, version vers }, nil } -func (source versionSourceEntry) values() (remote string, ref string, commit string) { - remote = strings.TrimSpace(source.Remote) - ref = strings.TrimSpace(source.Ref) - commit = strings.ToLower(strings.TrimSpace(source.Commit)) - if source.Git != nil { - if strings.TrimSpace(source.Git.Remote) != "" { - remote = strings.TrimSpace(source.Git.Remote) - } - if strings.TrimSpace(source.Git.Ref) != "" { - ref = strings.TrimSpace(source.Git.Ref) - } - if strings.TrimSpace(source.Git.Commit) != "" { - commit = strings.ToLower(strings.TrimSpace(source.Git.Commit)) - } +func (locator versionLocatorEntry) values(coordinate PackageHandleCoordinate) (remote string, ref string, commit string, err error) { + kind := strings.ToLower(strings.TrimSpace(locator.Kind)) + if kind != "" && kind != "git" { + return "", "", "", fmt.Errorf("registry version %s locator.kind must be %q", coordinate.String(), "git") } + remote = strings.TrimSpace(locator.Repository) + ref = strings.TrimSpace(locator.Ref) + commit = strings.ToLower(strings.TrimSpace(locator.Commit)) - return remote, ref, commit + return remote, ref, commit, nil } var commitPattern = regexp.MustCompile(`^(?:[0-9a-f]{40}|[0-9a-f]{64})$`) diff --git a/cmd/orbit/cli/registry/resolver_test.go b/cmd/orbit/cli/registry/resolver_test.go index aab21d2..2107d1d 100644 --- a/cmd/orbit/cli/registry/resolver_test.go +++ b/cmd/orbit/cli/registry/resolver_test.go @@ -24,13 +24,18 @@ func TestResolveExactPackageHandleCoordinateFromNamespaceIndex(t *testing.T) { "namespace: acme\n"+ "packages:\n"+ " docs:\n"+ + " handle: acme/docs\n"+ " status: active\n"+ + " package:\n"+ + " type: orbit\n"+ + " name: docs\n"+ + " source:\n"+ + " repository: https://example.com/acme/packages.git\n"+ " versions:\n"+ " 0.1.0:\n"+ - " package_type: orbit\n"+ - " package_identity: docs\n"+ - " source:\n"+ - " remote: https://example.com/acme/packages.git\n"+ + " locator:\n"+ + " kind: git\n"+ + " repository: https://example.com/acme/packages.git\n"+ " ref: orbit-template/docs\n"+ " commit: 0123456789abcdef0123456789abcdef01234567\n")) require.NoError(t, err) @@ -44,6 +49,41 @@ func TestResolveExactPackageHandleCoordinateFromNamespaceIndex(t *testing.T) { require.Empty(t, resolution.Warnings) } +func TestResolveExactPackageHandleCoordinateFromCatalogLocatorSchema(t *testing.T) { + t.Parallel() + + coordinate, err := registry.ParsePackageHandleCoordinate("zack-nova/product-lab@0.1.0") + require.NoError(t, err) + + resolution, err := registry.ResolveExactVersionFromNamespaceIndex(coordinate, []byte(""+ + "schema_version: 1\n"+ + "namespace: zack-nova\n"+ + "packages:\n"+ + " product-lab:\n"+ + " handle: zack-nova/product-lab\n"+ + " status: active\n"+ + " package:\n"+ + " type: harness\n"+ + " name: hyard-demo-product-lab\n"+ + " source:\n"+ + " repository: https://github.com/zack-nova/hyard-demo-product-lab\n"+ + " dist_tags:\n"+ + " latest: \"0.1.0\"\n"+ + " versions:\n"+ + " \"0.1.0\":\n"+ + " locator:\n"+ + " kind: git\n"+ + " repository: https://github.com/zack-nova/hyard-demo-product-lab\n"+ + " ref: harness-template/product-lab\n"+ + " commit: 7760299002a8ebd249899d5bacbf1340022fd25b\n")) + require.NoError(t, err) + require.Equal(t, ids.PackageTypeHarness, resolution.PackageType) + require.Equal(t, "hyard-demo-product-lab", resolution.PackageIdentity) + require.Equal(t, "https://github.com/zack-nova/hyard-demo-product-lab", resolution.SourceRemote) + require.Equal(t, "harness-template/product-lab", resolution.SourceRef) + require.Equal(t, "7760299002a8ebd249899d5bacbf1340022fd25b", resolution.SourceCommit) +} + func TestResolveExactPackageHandleCoordinateFailsClosedWhenVersionMissing(t *testing.T) { t.Parallel() @@ -55,13 +95,18 @@ func TestResolveExactPackageHandleCoordinateFailsClosedWhenVersionMissing(t *tes "namespace: acme\n"+ "packages:\n"+ " docs:\n"+ + " handle: acme/docs\n"+ " status: active\n"+ + " package:\n"+ + " type: orbit\n"+ + " name: docs\n"+ + " source:\n"+ + " repository: https://example.com/acme/packages.git\n"+ " versions:\n"+ " 0.1.0:\n"+ - " package_type: orbit\n"+ - " package_identity: docs\n"+ - " source:\n"+ - " remote: https://example.com/acme/packages.git\n"+ + " locator:\n"+ + " kind: git\n"+ + " repository: https://example.com/acme/packages.git\n"+ " ref: orbit-template/docs\n"+ " commit: 0123456789abcdef0123456789abcdef01234567\n")) require.Error(t, err) @@ -85,22 +130,26 @@ func TestResolvePackageHandleCoordinateResolvesNamespacedLatestDistTag(t *testin "namespace: acme\n" + "packages:\n" + " docs:\n" + + " handle: acme/docs\n" + " status: active\n" + + " package:\n" + + " type: orbit\n" + + " name: docs\n" + + " source:\n" + + " repository: https://example.com/acme/packages.git\n" + " dist_tags:\n" + " latest: 0.2.0\n" + " versions:\n" + " 0.1.0:\n" + - " package_type: orbit\n" + - " package_identity: docs\n" + - " source:\n" + - " remote: https://example.com/acme/packages.git\n" + + " locator:\n" + + " kind: git\n" + + " repository: https://example.com/acme/packages.git\n" + " ref: orbit-template/docs-old\n" + " commit: 0123456789abcdef0123456789abcdef01234567\n" + " 0.2.0:\n" + - " package_type: orbit\n" + - " package_identity: docs\n" + - " source:\n" + - " remote: https://example.com/acme/packages.git\n" + + " locator:\n" + + " kind: git\n" + + " repository: https://example.com/acme/packages.git\n" + " ref: orbit-template/docs\n" + " commit: abcdef0123456789abcdef0123456789abcdef01\n"), nil }), @@ -136,15 +185,20 @@ func TestResolvePackageHandleCoordinateResolvesCuratedLatestAlias(t *testing.T) "namespace: acme\n" + "packages:\n" + " docs:\n" + + " handle: acme/docs\n" + " status: active\n" + + " package:\n" + + " type: orbit\n" + + " name: docs\n" + + " source:\n" + + " repository: https://example.com/acme/packages.git\n" + " dist_tags:\n" + " latest: 0.2.0\n" + " versions:\n" + " 0.2.0:\n" + - " package_type: orbit\n" + - " package_identity: docs\n" + - " source:\n" + - " remote: https://example.com/acme/packages.git\n" + + " locator:\n" + + " kind: git\n" + + " repository: https://example.com/acme/packages.git\n" + " ref: orbit-template/docs\n" + " commit: abcdef0123456789abcdef0123456789abcdef01\n"), }, @@ -196,20 +250,24 @@ func TestResolvePackageHandleCoordinateDoesNotInferLatestFromHighestVersion(t *t "namespace: acme\n" + "packages:\n" + " docs:\n" + + " handle: acme/docs\n" + " status: active\n" + + " package:\n" + + " type: orbit\n" + + " name: docs\n" + + " source:\n" + + " repository: https://example.com/acme/packages.git\n" + " versions:\n" + " 0.1.0:\n" + - " package_type: orbit\n" + - " package_identity: docs\n" + - " source:\n" + - " remote: https://example.com/acme/packages.git\n" + + " locator:\n" + + " kind: git\n" + + " repository: https://example.com/acme/packages.git\n" + " ref: orbit-template/docs\n" + " commit: 0123456789abcdef0123456789abcdef01234567\n" + " 9.9.9:\n" + - " package_type: orbit\n" + - " package_identity: docs\n" + - " source:\n" + - " remote: https://example.com/acme/packages.git\n" + + " locator:\n" + + " kind: git\n" + + " repository: https://example.com/acme/packages.git\n" + " ref: orbit-template/docs-new\n" + " commit: abcdef0123456789abcdef0123456789abcdef01\n"), nil }), @@ -236,15 +294,20 @@ func TestResolvePackageHandleCoordinateUsesStaleCacheWhenLatestRegistryUnavailab "namespace: acme\n" + "packages:\n" + " docs:\n" + + " handle: acme/docs\n" + " status: active\n" + + " package:\n" + + " type: orbit\n" + + " name: docs\n" + + " source:\n" + + " repository: https://example.com/acme/packages.git\n" + " dist_tags:\n" + " latest: 0.1.0\n" + " versions:\n" + " 0.1.0:\n" + - " package_type: orbit\n" + - " package_identity: docs\n" + - " source:\n" + - " remote: https://example.com/acme/packages.git\n" + + " locator:\n" + + " kind: git\n" + + " repository: https://example.com/acme/packages.git\n" + " ref: orbit-template/docs\n" + " commit: 0123456789abcdef0123456789abcdef01234567\n"), nil }), @@ -333,13 +396,18 @@ func TestResolveExactPackageHandleCoordinateReturnsYankedStatus(t *testing.T) { "namespace: acme\n"+ "packages:\n"+ " docs:\n"+ + " handle: acme/docs\n"+ " status: yanked\n"+ + " package:\n"+ + " type: orbit\n"+ + " name: docs\n"+ + " source:\n"+ + " repository: https://example.com/acme/packages.git\n"+ " versions:\n"+ " 0.1.0:\n"+ - " package_type: orbit\n"+ - " package_identity: docs\n"+ - " source:\n"+ - " remote: https://example.com/acme/packages.git\n"+ + " locator:\n"+ + " kind: git\n"+ + " repository: https://example.com/acme/packages.git\n"+ " ref: orbit-template/docs\n"+ " commit: 0123456789abcdef0123456789abcdef01234567\n")) require.NoError(t, err) @@ -358,13 +426,18 @@ func TestResolveExactPackageHandleCoordinateWarnsForDeprecatedStatus(t *testing. "namespace: acme\n"+ "packages:\n"+ " docs:\n"+ + " handle: acme/docs\n"+ " status: deprecated\n"+ + " package:\n"+ + " type: orbit\n"+ + " name: docs\n"+ + " source:\n"+ + " repository: https://example.com/acme/packages.git\n"+ " versions:\n"+ " 0.1.0:\n"+ - " package_type: orbit\n"+ - " package_identity: docs\n"+ - " source:\n"+ - " remote: https://example.com/acme/packages.git\n"+ + " locator:\n"+ + " kind: git\n"+ + " repository: https://example.com/acme/packages.git\n"+ " ref: orbit-template/docs\n"+ " commit: 0123456789abcdef0123456789abcdef01234567\n")) require.NoError(t, err) @@ -419,13 +492,18 @@ func TestResolveExactPackageHandleCoordinateUsesVerifiedCacheWhenRegistryUnavail "namespace: acme\n" + "packages:\n" + " docs:\n" + + " handle: acme/docs\n" + " status: active\n" + + " package:\n" + + " type: orbit\n" + + " name: docs\n" + + " source:\n" + + " repository: https://example.com/acme/packages.git\n" + " versions:\n" + " 0.1.0:\n" + - " package_type: orbit\n" + - " package_identity: docs\n" + - " source:\n" + - " remote: https://example.com/acme/packages.git\n" + + " locator:\n" + + " kind: git\n" + + " repository: https://example.com/acme/packages.git\n" + " ref: orbit-template/docs\n" + " commit: 0123456789abcdef0123456789abcdef01234567\n"), nil }) @@ -508,13 +586,18 @@ func TestFreshBlockedStatusTakesPrecedenceOverActiveCache(t *testing.T) { "namespace: acme\n" + "packages:\n" + " docs:\n" + + " handle: acme/docs\n" + " status: blocked\n" + + " package:\n" + + " type: orbit\n" + + " name: docs\n" + + " source:\n" + + " repository: https://example.com/acme/packages.git\n" + " versions:\n" + " 0.1.0:\n" + - " package_type: orbit\n" + - " package_identity: docs\n" + - " source:\n" + - " remote: https://example.com/acme/packages.git\n" + + " locator:\n" + + " kind: git\n" + + " repository: https://example.com/acme/packages.git\n" + " ref: orbit-template/docs\n" + " commit: abcdef0123456789abcdef0123456789abcdef01\n"), nil }), diff --git a/docs/demos/codebase-developer-compose-runtime.md b/docs/demos/codebase-developer-compose-runtime.md index 2199527..5750718 100644 --- a/docs/demos/codebase-developer-compose-runtime.md +++ b/docs/demos/codebase-developer-compose-runtime.md @@ -54,14 +54,15 @@ Terminal display: ## Step 3: Install Three Orbit Workflows ```bash -hyard install https://github.com/zack-nova/hyard-demo-docs-orbit.git --ref orbit-template/docs --json -hyard install https://github.com/zack-nova/hyard-demo-review-orbit.git --ref orbit-template/review --json -hyard install https://github.com/zack-nova/hyard-demo-release-orbit.git --ref orbit-template/release --json +hyard install docs --json +hyard install review --json +hyard install release --json ``` Expected result: - `docs`, `review`, and `release` are installed as Orbit Packages. +- Harness Yard resolves each curated handle through the package registry. - `AGENTS.md`, `HUMANS.md`, and `docs/**` runtime content are written. - Each install leaves the runtime ready. @@ -77,6 +78,13 @@ Terminal display: "HUMANS.md", "docs/docs.md" ], + "source": { + "package_coordinate": "docs@latest", + "registry_provenance": { + "resolved_coordinate": "zack-nova/docs@0.1.0", + "source_ref": "orbit-template/docs" + } + }, "readiness": { "status": "ready" } diff --git a/docs/demos/orbit-template-branch-development.md b/docs/demos/orbit-template-branch-development.md index c9a127c..891c835 100644 --- a/docs/demos/orbit-template-branch-development.md +++ b/docs/demos/orbit-template-branch-development.md @@ -140,8 +140,12 @@ hyard publish orbit docs --push --remote origin --json Expected result: - `origin/orbit-template/docs` is updated. -- Users can install the new template with: +- Users can install the registered template by handle after the registry entry + or dist-tag has been updated to the new commit: ```bash -hyard install https://github.com/zack-nova/hyard-demo-docs-orbit.git --ref orbit-template/docs +hyard install docs ``` + +Until that registry update exists, the explicit Git locator remains the advanced +escape hatch for unpublished package revisions. diff --git a/docs/demos/runtime-user-fast-start.md b/docs/demos/runtime-user-fast-start.md index cc28b8d..d46d884 100644 --- a/docs/demos/runtime-user-fast-start.md +++ b/docs/demos/runtime-user-fast-start.md @@ -1,37 +1,48 @@ # Demo: Runtime User Fast Start This demo shows the shortest Runtime User path: clone a published Harness -Template, inspect it, and hand off to an Agent Framework. +Package by handle, inspect it, and hand off to an Agent Framework. -The demo uses the public Product Lab fixture: +The demo uses the curated Product Lab package handle: ```text -https://github.com/zack-nova/hyard-demo-product-lab.git -ref: harness-template/product-lab +product-lab ``` -## Step 1: Clone The Harness Template +## Step 1: Clone Product Lab By Handle Run this from the directory where you want the demo runtime folder created: ```bash -hyard clone https://github.com/zack-nova/hyard-demo-product-lab.git product-lab --ref harness-template/product-lab +hyard clone product-lab --json cd product-lab ``` Expected result: +- Harness Yard resolves the curated `product-lab` handle through the package + registry. - A new Harness Runtime appears in `product-lab`. - The runtime contains the `docs`, `review`, and `release` Orbit Workflows. +- The clone is pinned to the registry-resolved source commit. - The next action is Harness Start. Terminal display: ```json { - "harness_id": "product-lab", + "harness_id": "hyard-demo-product-lab", + "source": { + "package_coordinate": "product-lab@latest", + "registry_provenance": { + "resolved_coordinate": "zack-nova/product-lab@0.1.0", + "package_type": "harness", + "source_ref": "harness-template/product-lab" + } + }, "member_ids": ["docs", "release", "review"], "member_count": 3, + "bundle_count": 1, "readiness": { "status": "ready" }, @@ -43,6 +54,7 @@ Terminal display: ] } ``` + ## Step 2: Check Runtime Readiness ```bash diff --git a/docs/maintainers/package-registry-source-contract.md b/docs/maintainers/package-registry-source-contract.md index 49b783d..dcd8165 100644 --- a/docs/maintainers/package-registry-source-contract.md +++ b/docs/maintainers/package-registry-source-contract.md @@ -58,6 +58,45 @@ metadata, and validation evidence. handle uses `target` to point at a namespaced Package Handle and does not copy full version locator metadata. +Namespace package indexes use this shape: + +```yaml +schema_version: 1 +namespace: acme +packages: + docs: + handle: acme/docs + status: active + package: + type: orbit + name: docs + source: + repository: https://github.com/acme/docs-orbit + dist_tags: + latest: "0.1.0" + versions: + "0.1.0": + locator: + kind: git + repository: https://github.com/acme/docs-orbit + ref: orbit-template/docs + commit: 0123456789abcdef0123456789abcdef01234567 + validation: + remote_ref: refs/heads/orbit-template/docs + manifest: .harness/manifest.yaml + package_manifest: .harness/orbits/docs.yaml + package_identity: + type: orbit + name: docs +``` + +`package.type` is `orbit` or `harness`. `package.name` is the published package +identity validated from package-owned truth. `versions..locator` +contains the commit-pinned install locator. Registry resolvers should read this +catalog index schema directly; historical version-local +`package_type` / `package_identity` / `source.remote` entries are not the current +catalog schema. + ## Coordinate Rules Supported Package Handle Coordinate forms: @@ -187,6 +226,5 @@ not trust local validation evidence as authoritative. ## Implementation Notes -The exact YAML field names and CLI flag names can be finalized in the first -implementation slice, but changing the semantic rules in this document requires -updating the accepted ADR and the parent PRD issue. +Changing the semantic rules or catalog index field names in this document +requires updating the accepted ADR and the parent PRD issue. diff --git a/docs/quickstart.md b/docs/quickstart.md index f492baa..a31850c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -33,17 +33,16 @@ Run View is the default runtime-user presentation: it keeps authored scaffolding of the ordinary working tree and makes current runtime publication the recommended sharing path. -Package Handle Coordinates are the ordinary install path for published Harness -Packages and Orbit Packages. Use `namespace/name[@version-or-tag]`, not npm-style `@namespace/name`. +Package Handle Coordinates are the ordinary clone/install path for published +Harness Packages and Orbit Packages. Use `namespace/name[@version-or-tag]`, not +npm-style `@namespace/name`. Package Handle Coordinates are case-insensitive and normalize before resolution. -Create a runtime, install a published Harness Package by handle, and start the -agent handoff: +Clone a published Harness Package by handle and start the agent handoff: ```bash -hyard create runtime demo-runtime +hyard clone acme/frontend-lab demo-runtime cd demo-runtime -hyard install acme/frontend-lab hyard start --with codex ``` @@ -163,6 +162,8 @@ readiness-relevant. Install a reusable template or package: ```bash +hyard clone / [repo-name] +hyard clone [repo-name] hyard install / hyard install /@ hyard install diff --git a/docs/reference/release-surface.md b/docs/reference/release-surface.md index 1544f3f..0c5dc76 100644 --- a/docs/reference/release-surface.md +++ b/docs/reference/release-surface.md @@ -57,6 +57,8 @@ hyard install /@ hyard install /@latest hyard install hyard install [@latest] +hyard clone / [repo-name] +hyard clone [repo-name] hyard uninstall orbit hyard uninstall harness ``` @@ -75,6 +77,10 @@ hyard install docs Curated bare handles such as `docs` are reviewed aliases. `latest` is an explicit registry dist-tag, so `acme/docs` is equivalent to `acme/docs@latest`; documentation must not describe it as an inferred newest version or Git branch. +`hyard clone ` is the public shortcut for creating a new Harness Runtime +from a registry-backed Harness Package. If the handle resolves to an Orbit +Package, users should create or enter a runtime and use `hyard install ` +instead. If fresh registry data is unavailable, stale cached bare or `latest` resolutions may install with a warning. Mention `HYARD_CACHE_DIR` only in troubleshooting context for the user-level registry cache. @@ -249,12 +255,11 @@ Public demo examples should use Package Handle Coordinates once registry entries exist. Explicit Git locators are the advanced escape hatch when no registry entry is available. -Create a runtime, install a Harness Package by handle, and hand off to Codex: +Clone a Harness Package by handle and hand off to Codex: ```bash -hyard create runtime demo-runtime +hyard clone acme/frontend-lab demo-runtime cd demo-runtime -hyard install acme/frontend-lab hyard start --with codex ``` diff --git a/scripts/test_release_surface_hyard.sh b/scripts/test_release_surface_hyard.sh index a1232a9..ee6a1db 100644 --- a/scripts/test_release_surface_hyard.sh +++ b/scripts/test_release_surface_hyard.sh @@ -90,6 +90,7 @@ maintainer_testing_doc="$repo_root/docs/maintainers/testing-strategy.md" install_script="$repo_root/install.sh" goreleaser_config="$repo_root/.goreleaser.yaml" hyard_install_cmd="$repo_root/cmd/hyard/cli/install.go" +hyard_clone_cmd="$repo_root/cmd/hyard/cli/clone.go" hyard_registry_cmd="$repo_root/cmd/hyard/cli/registry.go" hyard_vars_cmd="$repo_root/cmd/hyard/cli/vars.go" harness_install_cmd="$repo_root/cmd/harness/cli/commands/install.go" @@ -109,6 +110,7 @@ for doc in \ "$install_script" \ "$goreleaser_config" \ "$hyard_install_cmd" \ + "$hyard_clone_cmd" \ "$hyard_registry_cmd" \ "$hyard_vars_cmd" \ "$harness_install_cmd" @@ -125,6 +127,7 @@ assert_contains "$quickstart_doc" "brew install hyard" assert_contains "$quickstart_doc" "raw.githubusercontent.com/zack-nova/harnessyard/main/install.sh" assert_contains "$quickstart_doc" "hyard --version" assert_contains "$quickstart_doc" "## Runtime User Path" +assert_contains "$quickstart_doc" "hyard clone acme/frontend-lab demo-runtime" assert_contains "$quickstart_doc" "hyard clone https://github.com/acme/harness-templates.git demo-runtime --ref harness-template/frontend-lab" assert_contains "$quickstart_doc" "hyard start --with codex" assert_contains "$quickstart_doc" "Run View is the default runtime-user presentation" @@ -139,7 +142,9 @@ assert_contains "$quickstart_doc" '`not_hyard_revision` exit non-zero' assert_contains "$quickstart_doc" "Audit is scoped to the current Git worktree" assert_contains "$quickstart_doc" "### Existing Repository Assembly" assert_contains "$quickstart_doc" "hyard init runtime" -assert_contains "$quickstart_doc" "Package Handle Coordinates are the ordinary install path" +assert_contains "$quickstart_doc" "Package Handle Coordinates are the ordinary clone/install path" +assert_contains "$quickstart_doc" "hyard clone / [repo-name]" +assert_contains "$quickstart_doc" "hyard clone [repo-name]" assert_contains "$quickstart_doc" "hyard install acme/frontend-lab" assert_contains "$quickstart_doc" "hyard install acme/docs" assert_contains "$quickstart_doc" "hyard install acme/docs@0.1.0" @@ -151,7 +156,8 @@ assert_contains "$quickstart_doc" "{{ vars.project_name }}" assert_contains "$quickstart_doc" "Runtime Bindings" assert_contains "$quickstart_doc" "Package Variables" assert_contains "$quickstart_doc" 'Package Handle Coordinates are case-insensitive' -assert_contains "$quickstart_doc" 'Use `namespace/name[@version-or-tag]`, not npm-style `@namespace/name`.' +assert_contains "$quickstart_doc" 'Use `namespace/name[@version-or-tag]`' +assert_contains "$quickstart_doc" 'npm-style `@namespace/name`.' assert_contains "$quickstart_doc" 'Bare handles such as `docs` are curated aliases' assert_contains "$quickstart_doc" '`latest` is an explicit registry dist-tag' assert_contains "$quickstart_doc" 'If fresh registry data cannot be fetched' @@ -290,9 +296,12 @@ assert_contains "$release_surface_doc" 'Public product documentation should teac assert_contains "$release_surface_doc" "brew tap zack-nova/tap" assert_contains "$release_surface_doc" "raw.githubusercontent.com/zack-nova/harnessyard/main/install.sh" assert_contains "$release_surface_doc" "hyard install " +assert_contains "$release_surface_doc" "hyard clone / [repo-name]" +assert_contains "$release_surface_doc" "hyard clone [repo-name]" assert_contains "$release_surface_doc" "hyard install /" assert_contains "$release_surface_doc" "hyard install /@" assert_contains "$release_surface_doc" "hyard install " +assert_contains "$release_surface_doc" 'hyard clone ` is the public shortcut' assert_contains "$release_surface_doc" "hyard install acme/docs" assert_contains "$release_surface_doc" "hyard install acme/docs@0.1.0" assert_contains "$release_surface_doc" "hyard install docs" @@ -316,6 +325,7 @@ assert_contains "$release_surface_doc" "hyard audit --json" assert_contains "$release_surface_doc" "Audit is scoped to the current Git worktree" assert_contains "$release_surface_doc" 'Orbit Package publish readiness on `hyard orbit prepare --check --json`' assert_contains "$release_surface_doc" "## Harness Start Demo Paths" +assert_contains "$release_surface_doc" "hyard clone acme/frontend-lab demo-runtime" assert_contains "$release_surface_doc" "hyard clone https://github.com/acme/harness-templates.git demo-runtime --ref harness-template/frontend-lab" assert_contains "$release_surface_doc" "hyard start --with codex" assert_contains "$release_surface_doc" "hyard init runtime" @@ -404,6 +414,12 @@ assert_contains "$hyard_install_cmd" "hyard install acme/docs@0.1.0" assert_contains "$hyard_install_cmd" "registry-source" assert_contains "$hyard_install_cmd" "allow-yanked" +assert_contains "$hyard_clone_cmd" "clone [repo-name]" +assert_contains "$hyard_clone_cmd" "hyard clone product-lab" +assert_contains "$hyard_clone_cmd" "hyard clone zack-nova/product-lab demo-runtime" +assert_contains "$hyard_clone_cmd" "registry-source" +assert_contains "$hyard_clone_cmd" "allow-yanked" + assert_contains "$harness_install_cmd" "Runtime Bindings YAML file" assert_contains "$harness_install_cmd" "hideInstallBindingCompatibilityFlags(cmd)"