Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `apm init` now creates `start.prompt.md` so `apm run start` works out of the box; Next Steps panel no longer references `apm compile` (#649)
- Propagate headers and environment variables through OpenCode MCP adapter with defensive copies to prevent mutation (#622)
### Changed

Expand Down
27 changes: 26 additions & 1 deletion src/apm_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@
_validate_plugin_name,
)

START_PROMPT_FILENAME = "start.prompt.md"

START_PROMPT_TEMPLATE = """\
---
name: start
---
You are a helpful assistant working on this project.

Help me understand the codebase and suggest improvements.
"""


@click.command(help="Initialize a new APM project")
@click.argument("project_name", required=False)
Expand Down Expand Up @@ -106,6 +117,16 @@ def init(ctx, project_name, yes, plugin, verbose):
# Create apm.yml (with devDependencies for plugin mode)
_create_minimal_apm_yml(config, plugin=plugin)

# Create start.prompt.md for regular projects (not plugin mode)
start_prompt_created = False
if not plugin:
start_prompt_path = Path(START_PROMPT_FILENAME)
if not start_prompt_path.exists():
start_prompt_path.write_text(START_PROMPT_TEMPLATE, encoding="utf-8")
start_prompt_created = True
else:
logger.progress(f"{START_PROMPT_FILENAME} already exists, skipping")

# Create plugin.json for plugin mode
if plugin:
_create_plugin_json(config)
Expand All @@ -119,13 +140,17 @@ def init(ctx, project_name, yes, plugin, verbose):
files_data = [
("*", APM_YML_FILENAME, "Project configuration"),
]
if start_prompt_created:
files_data.append(("*", START_PROMPT_FILENAME, "Starter prompt"))
if plugin:
files_data.append(("*", "plugin.json", "Plugin metadata"))
table = _create_files_table(files_data, title="Created Files")
console.print(table)
except (ImportError, NameError):
logger.progress("Created:")
click.echo(" * apm.yml - Project configuration")
if start_prompt_created:
click.echo(f" * {START_PROMPT_FILENAME} - Starter prompt")
if plugin:
click.echo(" * plugin.json - Plugin metadata")

Expand All @@ -141,7 +166,7 @@ def init(ctx, project_name, yes, plugin, verbose):
next_steps = [
"Install a runtime: apm runtime setup copilot",
"Add APM dependencies: apm install <owner>/<repo>",
"Compile agent context: apm compile",
"Edit your prompt: start.prompt.md (or .apm/prompts/start.prompt.md)",
"Run your first workflow: apm run start",
]

Expand Down
86 changes: 78 additions & 8 deletions tests/unit/test_init_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def teardown_method(self):
os.chdir(str(repo_root))

def test_init_current_directory(self):
"""Test initialization in current directory (minimal mode)."""
"""Test initialization in current directory."""
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
Expand All @@ -46,15 +46,16 @@ def test_init_current_directory(self):
assert result.exit_code == 0
assert "APM project initialized successfully!" in result.output
assert Path("apm.yml").exists()
# Minimal mode: no template files created
assert Path("start.prompt.md").exists()
# No extra template files created
assert not Path("hello-world.prompt.md").exists()
assert not Path("README.md").exists()
assert not Path(".apm").exists()
finally:
os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup

def test_init_explicit_current_directory(self):
"""Test initialization with explicit '.' argument (minimal mode)."""
"""Test initialization with explicit '.' argument."""
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
Expand All @@ -64,13 +65,14 @@ def test_init_explicit_current_directory(self):
assert result.exit_code == 0
assert "APM project initialized successfully!" in result.output
assert Path("apm.yml").exists()
# Minimal mode: no template files created
assert Path("start.prompt.md").exists()
# No extra template files created
assert not Path("hello-world.prompt.md").exists()
finally:
os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup

def test_init_new_directory(self):
"""Test initialization in new directory (minimal mode)."""
"""Test initialization in new directory."""
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
Expand All @@ -84,7 +86,8 @@ def test_init_new_directory(self):
assert project_path.exists()
assert project_path.is_dir()
assert (project_path / "apm.yml").exists()
# Minimal mode: no template files created
assert (project_path / "start.prompt.md").exists()
# No extra template files created
assert not (project_path / "hello-world.prompt.md").exists()
assert not (project_path / "README.md").exists()
assert not (project_path / ".apm").exists()
Expand Down Expand Up @@ -221,7 +224,7 @@ def test_init_existing_project_interactive_cancel(self):
os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup

def test_init_validates_project_structure(self):
"""Test that init creates minimal project structure."""
"""Test that init creates expected project structure."""
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
Expand All @@ -243,7 +246,9 @@ def test_init_validates_project_structure(self):
assert "scripts" in config
assert config["scripts"] == {}

# Minimal mode: no template files created
# start.prompt.md created
assert (project_path / "start.prompt.md").exists()
# No extra template files created
assert not (project_path / "hello-world.prompt.md").exists()
assert not (project_path / "README.md").exists()
assert not (project_path / ".apm").exists()
Expand Down Expand Up @@ -296,6 +301,71 @@ def test_init_does_not_create_skill_md(self):
finally:
os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup

def test_init_creates_start_prompt_md(self):
"""Test that init creates start.prompt.md with correct content."""
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
result = self.runner.invoke(cli, ["init", "--yes"])

assert result.exit_code == 0
prompt_path = Path("start.prompt.md")
assert prompt_path.exists()

content = prompt_path.read_text(encoding="utf-8")
assert "---" in content
assert "name: start" in content
assert "You are a helpful assistant" in content
finally:
os.chdir(self.original_dir)

def test_init_does_not_overwrite_existing_start_prompt(self):
"""Test that init preserves existing start.prompt.md (brownfield)."""
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
# Create existing start.prompt.md with custom content
Path("start.prompt.md").write_text(
"---\nname: start\n---\nMy custom prompt\n", encoding="utf-8"
)

result = self.runner.invoke(cli, ["init", "--yes"])

assert result.exit_code == 0
content = Path("start.prompt.md").read_text(encoding="utf-8")
assert "My custom prompt" in content
assert "start.prompt.md already exists" in result.output
finally:
os.chdir(self.original_dir)

def test_init_next_steps_no_compile(self):
"""Test that next steps do not reference apm compile."""
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
result = self.runner.invoke(cli, ["init", "--yes"])

assert result.exit_code == 0
assert "apm compile" not in result.output
assert "Edit your prompt" in result.output
assert "start.prompt.md" in result.output
assert "apm run start" in result.output
finally:
os.chdir(self.original_dir)

def test_init_created_files_table_includes_start_prompt(self):
"""Test that Created Files table lists start.prompt.md."""
with tempfile.TemporaryDirectory() as tmp_dir:
os.chdir(tmp_dir)
try:
result = self.runner.invoke(cli, ["init", "--yes"])

assert result.exit_code == 0
# start.prompt.md appears in the Created Files table
assert "start.prompt.md" in result.output
finally:
os.chdir(self.original_dir)



class TestPluginNameValidation:
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/test_init_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,19 @@ def test_plugin_json_ends_with_newline(self):
finally:
os.chdir(self.original_dir)

def test_plugin_does_not_create_start_prompt(self):
"""start.prompt.md is NOT created in plugin mode."""
with tempfile.TemporaryDirectory() as tmp_dir:
project_dir = Path(tmp_dir) / "my-plugin"
project_dir.mkdir()
os.chdir(project_dir)
try:
result = self.runner.invoke(cli, ["init", "--plugin", "--yes"])
assert result.exit_code == 0, result.output
assert not Path("start.prompt.md").exists()
finally:
os.chdir(self.original_dir)

def test_plugin_apm_yml_has_dependencies(self):
"""apm.yml created with --plugin still has regular dependencies section."""
with tempfile.TemporaryDirectory() as tmp_dir:
Expand Down
Loading