cmd/cue/cmd: control task dependency using before and after fields

Currently we resolve task dependency by checking reference to incomplete task variable. We cannot do things like `create a file first, then run a command with it`.

For example this command would not work cause `write` and `ansible` will be executed at the same time.

```
command: play: {
	task: write: file.Create & {
		filename: "playbook.yml"
		contents:  yaml.MarshalStream([playbook])
	}
	task: ansible: exec.Run & {
		cmd:    "ansible-playbook playbook.yml"
		stdout: string
	}
	task: display: cli.Print & {
		text: task.ansible.stdout
	}
}
```

Using this PR, we could declare dependency and reverse dependency using `before` and `after` fields. The value could be either a task or a list of tasks. Now we can rewrite the command like this to make it work.

```
command: play: {
	task: write: file.Create & {
		filename: "playbook.yml"
		contents:  yaml.MarshalStream([playbook])
	}
	task: ansible: exec.Run & {
		cmd:    "ansible-playbook playbook.yml"
		stdout: string
		after: task.write
	}
	task: display: cli.Print & {
		text: task.ansible.stdout
	}
}
```

Closes #200
https://github.com/cuelang/cue/pull/200

GitOrigin-RevId: 362c703d71dd425654f52d3e7aecf811cffa4a19
Change-Id: I075b871745f2b5dc18788d2d0c02d0623fced6c4
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4340
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/custom.go b/cmd/cue/cmd/custom.go
index b8670a4..f508ae8 100644
--- a/cmd/cue/cmd/custom.go
+++ b/cmd/cue/cmd/custom.go
@@ -127,6 +127,16 @@
 	return err
 }
 
+func isTask(index map[taskKey]*task, root *cue.Instance, value cue.Value) (*task, bool) {
+	inst, path := value.Reference()
+	if path != nil && inst == root {
+		if task, ok := index[keyForReference(path)]; ok {
+			return task, true
+		}
+	}
+	return nil, false
+}
+
 // executeTasks runs user-defined tasks as part of a user-defined command.
 //
 // All tasks are started at once, but will block until tasks that they depend
@@ -155,9 +165,48 @@
 	// Mark dependencies for unresolved nodes.
 	for _, t := range queue {
 		task := tasks.Lookup(t.name)
+
+		// Inject dependency in `$after` field
+		after := task.Lookup("$after")
+		if after.Err() == nil {
+			if dep, ok := isTask(index, root, after); ok {
+				t.dep[dep] = true
+			}
+			iter, err := after.List()
+			if err == nil {
+				for iter.Next() {
+					if dep, ok := isTask(index, root, iter.Value()); ok {
+						t.dep[dep] = true
+					}
+				}
+			}
+		}
+
+		// Inject reverse dependency in `$before` field
+		before := task.Lookup("$before")
+		if before.Err() == nil {
+			if dep, ok := isTask(index, root, before); ok {
+				dep.dep[t] = true
+			}
+			iter, err := before.List()
+			if err == nil {
+				for iter.Next() {
+					if dep, ok := isTask(index, root, iter.Value()); ok {
+						dep.dep[t] = true
+					}
+				}
+			}
+		}
+
 		task.Walk(func(v cue.Value) bool {
+			if v == task {
+				return true
+			}
+			if (after.Err() == nil && v.Equals(after)) || (before.Err() == nil && v.Equals(before)) {
+				return false
+			}
 			for _, r := range appendReferences(nil, root, v) {
-				if dep, ok := index[keyForReference(r)]; ok {
+				if dep, ok := index[keyForReference(r)]; ok && t != dep {
 					v := root.Lookup(r...)
 					if v.IsIncomplete() && v.Kind() != cue.StructKind {
 						t.dep[dep] = true
diff --git a/cmd/cue/cmd/testdata/script/cmd_after.txt b/cmd/cue/cmd/testdata/script/cmd_after.txt
new file mode 100644
index 0000000..6849774
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/cmd_after.txt
@@ -0,0 +1,40 @@
+cue cmd after
+cmp stdout expect-stdout
+
+-- expect-stdout --
+true
+
+-- after_tool.cue --
+package home
+
+import (
+	"tool/exec"
+	"tool/cli"
+	"strings"
+)
+
+command: after: {
+	task: {
+		t1: exec.Run & {
+			cmd: ["sh", "-c", "sleep 2; date +%s"]
+			stdout: string
+		}
+		t2: exec.Run & {
+			cmd: ["sh", "-c", "date +%s"]
+			stdout: string
+			$after: task.t1
+		}
+		t3: exec.Run & {
+			cmd: ["sh", "-c", "a=\(strings.TrimSpace(task.t1.stdout));b=\(strings.TrimSpace(task.t2.stdout));if [ $a -le $b ]; then echo 'true'; fi"]
+			stdout: string
+		}
+		t4: cli.Print & {
+				text: task.t3.stdout
+		}
+	}
+}
+
+-- task.cue --
+package home
+
+-- cue.mod --
diff --git a/cmd/cue/cmd/testdata/script/cmd_before.txt b/cmd/cue/cmd/testdata/script/cmd_before.txt
new file mode 100644
index 0000000..762509c
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/cmd_before.txt
@@ -0,0 +1,45 @@
+cue cmd before
+cmp stdout expect-stdout
+
+-- expect-stdout --
+true
+
+-- before_tool.cue --
+package home
+
+import (
+	"tool/exec"
+	"tool/cli"
+	"strings"
+)
+
+command: before: {
+	task: {
+		t1: exec.Run & {
+			cmd: ["sh", "-c", "sleep 2; date +%s"]
+			stdout: string
+			$before: [task.t2, task.t3]
+		}
+		t2: exec.Run & {
+			cmd: ["sh", "-c", "date +%s"]
+			stdout: string
+			$before: task.t4
+		}
+		t3: exec.Run & {
+			cmd: ["sh", "-c", "date +%s"]
+			stdout: string
+		}
+		t4: exec.Run & {
+			cmd: ["sh", "-c", "a=\(strings.TrimSpace(task.t1.stdout));b=\(strings.TrimSpace(task.t2.stdout));if [ $a -le $b ]; then echo 'true'; fi"]
+			stdout: string
+		}
+		t5: cli.Print & {
+				text: task.t4.stdout
+		}
+	}
+}
+
+-- task.cue --
+package home
+
+-- cue.mod --
diff --git a/cmd/cue/cmd/testdata/script/cmd_ref.txt b/cmd/cue/cmd/testdata/script/cmd_ref.txt
new file mode 100644
index 0000000..5141b3f
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/cmd_ref.txt
@@ -0,0 +1,29 @@
+cue cmd ref
+cmp stdout expect-stdout
+
+-- expect-stdout --
+hello
+
+-- task_tool.cue --
+package home
+
+import (
+	"tool/cli"
+)
+
+command: ref: {
+	task: {
+		t1: exec.Run & {
+				ref: task.t1.stdout
+				cmd: ["sh", "-c", "echo hello"]
+				stdout: string
+		}
+		t2: cli.Print & {
+			text: task.t1.stdout
+		}
+	}
+}
+
+-- task.cue --
+package home
+-- cue.mod --