feat(v0.5): scaffold project subdirectory and set launch_dir in task.yaml
ctask new --project now creates an empty subdirectory named after the final suffixed slug inside the workspace root, and sets meta.LaunchDir to that slug. Task workspaces are unchanged. Seeds do not target the subdirectory — the user populates it with their project code and their own CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,19 @@ func Create(opts CreateOpts) (*CreateResult, error) {
|
||||
WorkspacePath: wsDir,
|
||||
ArchivedAt: nil,
|
||||
}
|
||||
if opts.IsProject {
|
||||
meta.LaunchDir = actualSlug
|
||||
}
|
||||
|
||||
// v0.5: project workspaces get an empty subdirectory named after the
|
||||
// final suffixed slug. ctask does not seed any files inside it; the
|
||||
// user populates it with their project code + their own CLAUDE.md.
|
||||
if opts.IsProject {
|
||||
projDir := filepath.Join(wsDir, actualSlug)
|
||||
if err := os.MkdirAll(projDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("creating project subdir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// task.yaml is always written by ctask, AFTER the seed step, so seeds can
|
||||
// never inject a stale or malformed task.yaml.
|
||||
|
||||
@@ -309,3 +309,83 @@ func TestCreateCollisionSuffix(t *testing.T) {
|
||||
t.Errorf("collision slug: got %q, want \"same-name-2\"", meta2.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProjectScaffoldsSubdirAndSetsLaunchDir(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
res, err := Create(CreateOpts{
|
||||
Root: root,
|
||||
Title: "litlink-v2",
|
||||
Category: "projects",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
IsProject: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
subdir := filepath.Join(res.Path, res.Meta.Slug)
|
||||
info, err := os.Stat(subdir)
|
||||
if err != nil {
|
||||
t.Fatalf("project subdir missing: %v", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Errorf("project subdir exists but is not a directory")
|
||||
}
|
||||
entries, _ := os.ReadDir(subdir)
|
||||
if len(entries) != 0 {
|
||||
t.Errorf("project subdir must be empty, got %d entries", len(entries))
|
||||
}
|
||||
if res.Meta.LaunchDir != res.Meta.Slug {
|
||||
t.Errorf("LaunchDir: got %q, want slug %q", res.Meta.LaunchDir, res.Meta.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProjectSubdirMatchesSuffixedSlug(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
first, err := Create(CreateOpts{
|
||||
Root: root, Title: "dup", Category: "projects",
|
||||
Mode: "local", Agent: "claude", IsProject: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("first Create: %v", err)
|
||||
}
|
||||
if first.Meta.Slug != "dup" {
|
||||
t.Fatalf("first slug: got %q, want %q", first.Meta.Slug, "dup")
|
||||
}
|
||||
|
||||
second, err := Create(CreateOpts{
|
||||
Root: root, Title: "dup", Category: "projects",
|
||||
Mode: "local", Agent: "claude", IsProject: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("second Create: %v", err)
|
||||
}
|
||||
if second.Meta.Slug == "dup" {
|
||||
t.Fatalf("expected suffixed slug, got %q", second.Meta.Slug)
|
||||
}
|
||||
subdir := filepath.Join(second.Path, second.Meta.Slug)
|
||||
if _, err := os.Stat(subdir); err != nil {
|
||||
t.Errorf("subdir %q missing: %v", second.Meta.Slug, err)
|
||||
}
|
||||
if second.Meta.LaunchDir != second.Meta.Slug {
|
||||
t.Errorf("LaunchDir: got %q, want %q", second.Meta.LaunchDir, second.Meta.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTaskDoesNotScaffoldSubdir(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
res, err := Create(CreateOpts{
|
||||
Root: root, Title: "hello", Category: "general",
|
||||
Mode: "local", Agent: "claude", IsProject: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
subdir := filepath.Join(res.Path, res.Meta.Slug)
|
||||
if _, err := os.Stat(subdir); err == nil {
|
||||
t.Errorf("task workspace should not have a slug-named subdir; found %s", subdir)
|
||||
}
|
||||
if res.Meta.LaunchDir != "" {
|
||||
t.Errorf("LaunchDir: got %q, want empty", res.Meta.LaunchDir)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user