Skip to content

Commit 289167d

Browse files
committed
feat: implement extension build and test commands
- Add 'efctl env extension build' to compile Move contracts - Add 'efctl env extension test' to run Move contract tests - Extract common environment preparation logic in pkg/builder - Fix Move compilation errors in turret_aggressor_first extension - Update E2E tests and project TODO.md
1 parent a6e45e8 commit 289167d

11 files changed

Lines changed: 324 additions & 21 deletions

File tree

TODO.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ Feature ideas and improvements for `efctl`.
1212

1313
## Builder Flow
1414

15-
- [ ] **`efctl env extension test [contract-path]`** — Run `sui move test` for a Move contract inside the container, so developers don't need to `shell` in or use `env run`.
16-
- [ ] **`efctl env extension build [contract-path]`** — Compile a Move contract without publishing, catching errors earlier.
17-
- [ ] **`efctl env extension list`** — List published extensions with their package IDs, config IDs, and status from the `.env`.
15+
- [x] **`efctl env extension test [contract-path]`** — Run `sui move test` for a Move contract inside the container, so developers don't need to `shell` in or use `env run`.
16+
- [x] **`efctl env extension build [contract-path]`** — Compile a Move contract without publishing, catching errors earlier.
17+
- [x] **`efctl env extension list`** — List published extensions with their package IDs, config IDs, and status from the `.env`.
1818

1919
## GraphQL & Chain Interaction
2020

@@ -24,14 +24,14 @@ Feature ideas and improvements for `efctl`.
2424

2525
## Developer Experience
2626

27-
- [ ] **`efctl env init`** — Scaffold a new project directory with a starter `efctl.yaml`, Move contract template, and directory structure, reducing manual boilerplate for new builders.
28-
- [ ] **`efctl doctor`** — Comprehensive diagnostic that checks prerequisites, port conflicts, Docker daemon health, Sui client config, disk space, and version compatibility — then outputs a shareable report.
27+
- [x] **`efctl env init`** — Scaffold a new project directory with a starter `efctl.yaml`, Move contract template, and directory structure, reducing manual boilerplate for new builders. (Implemented as `efctl init`).
28+
- [x] **`efctl doctor`** — Comprehensive diagnostic that checks prerequisites, port conflicts, Docker daemon health, Sui client config, disk space, and version compatibility — then outputs a shareable report.
2929
- [x] **`efctl completion`** — Shell completion generation (bash/zsh/fish/powershell) via Cobra's built-in `GenBashCompletion` etc.
3030

3131
## Deployment & Networking
3232

3333
- [ ] **Testnet/Devnet deployment flow** — Currently `env up` is localnet-only. A `--network testnet` flag (or `efctl env deploy --network testnet`) would bridge the gap for staging.
34-
- [ ] **`efctl sui faucet [address]`** — Request test tokens from the localnet or testnet faucet without leaving the CLI.
34+
- [x] **`efctl sui faucet [address]`** — Request test tokens from the localnet or testnet faucet without leaving the CLI. (Implemented as `efctl env faucet`).
3535

3636
## Operational
3737

cmd/extension_build.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
6+
"efctl/pkg/builder"
7+
"efctl/pkg/container"
8+
"efctl/pkg/ui"
9+
"efctl/pkg/validate"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var extensionBuildCmd = &cobra.Command{
14+
Use: "build [extension-path]",
15+
Short: "Compile a Move contract without publishing",
16+
Long: `Compiles the specified extension contract (path relative to /workspace) inside the container, catching errors earlier.`,
17+
Args: cobra.ExactArgs(1),
18+
Run: func(cmd *cobra.Command, args []string) {
19+
extensionPath := args[0]
20+
if err := validate.Network(envNetwork); err != nil {
21+
ui.Error.Println(err.Error())
22+
os.Exit(1)
23+
}
24+
25+
c, err := container.NewClient()
26+
if err != nil {
27+
ui.Error.Println("Failed to create container client: " + err.Error())
28+
os.Exit(1)
29+
}
30+
31+
candidate, err := builder.GetCandidate(workspacePath, extensionPath)
32+
if err != nil {
33+
ui.Error.Printf("Error: extension %q not found.\n\n", extensionPath)
34+
closest := builder.FindClosestMatch(workspacePath, extensionPath)
35+
if len(closest) > 0 {
36+
ui.Info.Println("Did you mean:")
37+
for _, match := range closest {
38+
ui.Info.Printf(" - %s\n", match)
39+
}
40+
}
41+
os.Exit(1)
42+
}
43+
44+
if err := builder.BuildExtension(c, workspacePath, envNetwork, candidate); err != nil {
45+
ui.Error.Println("Build failed: " + err.Error())
46+
os.Exit(1)
47+
}
48+
},
49+
}
50+
51+
func init() {
52+
extensionBuildCmd.Flags().StringVarP(&envNetwork, "network", "n", "localnet", "The network to build for (localnet or testnet)")
53+
extensionCmd.AddCommand(extensionBuildCmd)
54+
}

cmd/extension_test_cli.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
6+
"efctl/pkg/builder"
7+
"efctl/pkg/container"
8+
"efctl/pkg/ui"
9+
"efctl/pkg/validate"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var extensionTestCmd = &cobra.Command{
14+
Use: "test [extension-path]",
15+
Short: "Run sui move test for a Move contract",
16+
Long: `Runs 'sui move test' for the specified extension contract (path relative to /workspace) inside the container.`,
17+
Args: cobra.ExactArgs(1),
18+
Run: func(cmd *cobra.Command, args []string) {
19+
extensionPath := args[0]
20+
if err := validate.Network(envNetwork); err != nil {
21+
ui.Error.Println(err.Error())
22+
os.Exit(1)
23+
}
24+
25+
c, err := container.NewClient()
26+
if err != nil {
27+
ui.Error.Println("Failed to create container client: " + err.Error())
28+
os.Exit(1)
29+
}
30+
31+
candidate, err := builder.GetCandidate(workspacePath, extensionPath)
32+
if err != nil {
33+
ui.Error.Printf("Error: extension %q not found.\n\n", extensionPath)
34+
closest := builder.FindClosestMatch(workspacePath, extensionPath)
35+
if len(closest) > 0 {
36+
ui.Info.Println("Did you mean:")
37+
for _, match := range closest {
38+
ui.Info.Printf(" - %s\n", match)
39+
}
40+
}
41+
os.Exit(1)
42+
}
43+
44+
if err := builder.TestExtension(c, workspacePath, envNetwork, candidate); err != nil {
45+
ui.Error.Println("Tests failed: " + err.Error())
46+
os.Exit(1)
47+
}
48+
},
49+
}
50+
51+
func init() {
52+
extensionTestCmd.Flags().StringVarP(&envNetwork, "network", "n", "localnet", "The network to test for (localnet or testnet)")
53+
extensionCmd.AddCommand(extensionTestCmd)
54+
}

docs/efctl_env_extension.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ The extension command groups operations defined in the EVE Frontier Builder Flow
2424
### SEE ALSO
2525

2626
* [efctl env](efctl_env.md) - Manage the local Sui development environment
27+
* [efctl env extension build](efctl_env_extension_build.md) - Compile a Move contract without publishing
2728
* [efctl env extension list](efctl_env_extension_list.md) - List all available extensions in the workspace
2829
* [efctl env extension publish](efctl_env_extension_publish.md) - Publish a custom extension contract
30+
* [efctl env extension test](efctl_env_extension_test.md) - Run sui move test for a Move contract
2931

docs/efctl_env_extension_build.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## efctl env extension build
2+
3+
Compile a Move contract without publishing
4+
5+
### Synopsis
6+
7+
Compiles the specified extension contract (path relative to /workspace) inside the container, catching errors earlier.
8+
9+
```
10+
efctl env extension build [extension-path] [flags]
11+
```
12+
13+
### Options
14+
15+
```
16+
-h, --help help for build
17+
-n, --network string The network to build for (localnet or testnet) (default "localnet")
18+
```
19+
20+
### Options inherited from parent commands
21+
22+
```
23+
--config-file string Path to the efctl.yaml or efctl.yml configuration file (default "efctl.yaml")
24+
--debug Enable verbose debug logging
25+
--no-progress Disable the progress spinner for cleaner CI output
26+
-w, --workspace string Path to the workspace directory (default ".")
27+
```
28+
29+
### SEE ALSO
30+
31+
* [efctl env extension](efctl_env_extension.md) - Manage the builder-scaffold extension flow
32+

docs/efctl_env_extension_test.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## efctl env extension test
2+
3+
Run sui move test for a Move contract
4+
5+
### Synopsis
6+
7+
Runs 'sui move test' for the specified extension contract (path relative to /workspace) inside the container.
8+
9+
```
10+
efctl env extension test [extension-path] [flags]
11+
```
12+
13+
### Options
14+
15+
```
16+
-h, --help help for test
17+
-n, --network string The network to test for (localnet or testnet) (default "localnet")
18+
```
19+
20+
### Options inherited from parent commands
21+
22+
```
23+
--config-file string Path to the efctl.yaml or efctl.yml configuration file (default "efctl.yaml")
24+
--debug Enable verbose debug logging
25+
--no-progress Disable the progress spinner for cleaner CI output
26+
-w, --workspace string Path to the workspace directory (default ".")
27+
```
28+
29+
### SEE ALSO
30+
31+
* [efctl env extension](efctl_env_extension.md) - Manage the builder-scaffold extension flow
32+

pkg/builder/build.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package builder
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"efctl/pkg/container"
9+
"efctl/pkg/ui"
10+
)
11+
12+
// BuildExtension compiles the Move contract inside the container without publishing.
13+
func BuildExtension(c container.ContainerClient, workspace string, network string, candidate PublishCandidate) error {
14+
if err := PrepareExtensionEnv(c, workspace, network); err != nil {
15+
return err
16+
}
17+
18+
ui.Info.Printf("Building extension contract from %s...\n", candidate.HostPath)
19+
ui.Info.Printf("Executing build inside container at %s...\n", candidate.ContainerPath)
20+
21+
buildCmd := fmt.Sprintf("cd %s && sui move build --build-env testnet", candidate.ContainerPath)
22+
23+
ui.Warn.Println("Build logging will be piped below:")
24+
25+
output, err := c.ExecCapture(context.Background(), container.ContainerSuiPlayground, []string{"/bin/bash", "-c", buildCmd})
26+
if output != "" {
27+
fmt.Print(output)
28+
}
29+
if err != nil {
30+
if strings.Contains(output, "Build error") || strings.Contains(output, "Compilation error") {
31+
return fmt.Errorf("build failed due to compilation errors")
32+
}
33+
return fmt.Errorf("build command failed: %w", err)
34+
}
35+
36+
ui.Success.Println("Extension contract built successfully.")
37+
return nil
38+
}

pkg/builder/publish.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ type PublishCandidate struct {
4444

4545
const worldDependencyMarker = "world = {"
4646

47-
// PublishExtension publishes the custom extension to the smart assembly testnet
48-
// and updates the builder-scaffold/.env with the extracted package IDs.
49-
func PublishExtension(c container.ContainerClient, workspace string, network string, candidate PublishCandidate) error {
47+
// PrepareExtensionEnv initializes the environment, repairs it if mismatched, and cleans stale files.
48+
func PrepareExtensionEnv(c container.ContainerClient, workspace string, network string) error {
5049
// Automatically initialize/sync the builder-scaffold environment with world artifacts
5150
if err := InitExtensionEnv(workspace, network); err != nil {
5251
return fmt.Errorf("failed to initialize extension environment: %w", err)
@@ -56,6 +55,22 @@ func PublishExtension(c container.ContainerClient, workspace string, network str
5655
return err
5756
}
5857

58+
// Clean stale Move.lock files before building/testing/publishing to avoid framework drift issues
59+
setup.CleanStaleMoveLocks(workspace)
60+
if err := setup.PatchBuilderExampleMoveTomls(workspace); err != nil {
61+
return err
62+
}
63+
64+
return nil
65+
}
66+
67+
// PublishExtension publishes the custom extension to the smart assembly testnet
68+
// and updates the builder-scaffold/.env with the extracted package IDs.
69+
func PublishExtension(c container.ContainerClient, workspace string, network string, candidate PublishCandidate) error {
70+
if err := PrepareExtensionEnv(c, workspace, network); err != nil {
71+
return err
72+
}
73+
5974
ui.Info.Printf("Publishing extension contract from %s...\n", candidate.HostPath)
6075

6176
ui.Info.Printf("Executing publish inside container at %s...\n", candidate.ContainerPath)
@@ -65,12 +80,6 @@ func PublishExtension(c container.ContainerClient, workspace string, network str
6580
return err
6681
}
6782

68-
// Clean stale Move.lock files before publishing to avoid framework drift issues
69-
setup.CleanStaleMoveLocks(workspace)
70-
if err := setup.PatchBuilderExampleMoveTomls(workspace); err != nil {
71-
return err
72-
}
73-
7483
ui.Warn.Println("Publish logging will be piped below:")
7584

7685
output, err := c.ExecCapture(context.Background(), container.ContainerSuiPlayground, []string{"/bin/bash", "-c", publishCmd})

pkg/builder/test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package builder
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"efctl/pkg/container"
9+
"efctl/pkg/ui"
10+
)
11+
12+
// TestExtension runs sui move test for the Move contract inside the container.
13+
func TestExtension(c container.ContainerClient, workspace string, network string, candidate PublishCandidate) error {
14+
if err := PrepareExtensionEnv(c, workspace, network); err != nil {
15+
return err
16+
}
17+
18+
ui.Info.Printf("Testing extension contract from %s...\n", candidate.HostPath)
19+
ui.Info.Printf("Executing test inside container at %s...\n", candidate.ContainerPath)
20+
21+
testCmd := fmt.Sprintf("cd %s && sui move test --build-env testnet", candidate.ContainerPath)
22+
23+
ui.Warn.Println("Test logging will be piped below:")
24+
25+
output, err := c.ExecCapture(context.Background(), container.ContainerSuiPlayground, []string{"/bin/bash", "-c", testCmd})
26+
if output != "" {
27+
fmt.Print(output)
28+
}
29+
if err != nil {
30+
if strings.Contains(output, "Test failures") {
31+
return fmt.Errorf("tests failed")
32+
}
33+
return fmt.Errorf("test command failed: %w", err)
34+
}
35+
36+
ui.Success.Println("Extension contract tests passed.")
37+
return nil
38+
}

0 commit comments

Comments
 (0)