From 3a46d2dd79e0b2678fe7da2ce18ca62fae7167d2 Mon Sep 17 00:00:00 2001 From: anguohui Date: Tue, 7 Jul 2026 17:34:14 +0800 Subject: [PATCH] feat(apps): add --source-path flag to +init for existing source file incorporation --- shortcuts/apps/apps_init.go | 23 +++++++++++++++-------- shortcuts/apps/apps_init_test.go | 20 ++++++++++---------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/shortcuts/apps/apps_init.go b/shortcuts/apps/apps_init.go index d730d2874..14778e3a7 100644 --- a/shortcuts/apps/apps_init.go +++ b/shortcuts/apps/apps_init.go @@ -77,6 +77,7 @@ var AppsInit = common.Shortcut{ {Name: "app-id", Desc: "app ID"}, {Name: "dir", Desc: "clone target directory; absolute or relative path (default ./)"}, {Name: "template", Desc: "code-init template for an empty repo; optional — if omitted, derived from the app's tech stack"}, + {Name: "source-path", Desc: "path to existing source files (e.g. HTML output from an agent) to incorporate into the initialized project"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { @@ -326,13 +327,13 @@ func isEmptyRepo(ctx context.Context, dir string) (bool, error) { // runScaffold runs the npx scaffolding step inside the cloned repo (cwd=dir). // Empty repo -> `app init`; non-empty -> `app sync` + meta app_id patch + // conditional `skills sync`. Returns "init" or "upgrade". -func runScaffold(ctx context.Context, dir, appID, appType, explicitTemplate string) (string, error) { +func runScaffold(ctx context.Context, dir, appID, appType, explicitTemplate, sourcePath string) (string, error) { empty, err := isEmptyRepo(ctx, dir) if err != nil { return "", err } if empty { - args := scaffoldInitArgs(appType, appID, explicitTemplate) + args := scaffoldInitArgs(appType, appID, explicitTemplate, sourcePath) if _, stderr, err := initRunner.Run(ctx, dir, "npx", args...); err != nil { return "", appsExternalToolError(err, "npx app init failed: %s", gitErr(stderr, err)) } @@ -355,15 +356,20 @@ func runScaffold(ctx context.Context, dir, appID, appType, explicitTemplate stri // scaffoldInitArgs builds the npx argument list for `app init`. An explicit // template wins; otherwise, when appType is available, it is passed as // --template; empty appType falls back to --template defaultTemplate. -func scaffoldInitArgs(appType, appID, explicitTemplate string) []string { +// sourcePath is appended as --source-path when non-empty. +func scaffoldInitArgs(appType, appID, explicitTemplate, sourcePath string) []string { base := []string{"-y", "--prefer-online", miaodaCLIPkg, "app", "init"} if explicitTemplate != "" { - return append(base, "--template", explicitTemplate, "--app-id", appID) + base = append(base, "--template", explicitTemplate, "--app-id", appID) + } else if appType != "" { + base = append(base, "--template", appType, "--app-id", appID) + } else { + base = append(base, "--template", defaultTemplate, "--app-id", appID) } - if appType != "" { - return append(base, "--template", appType, "--app-id", appID) + if sourcePath != "" { + base = append(base, "--source-path", sourcePath) } - return append(base, "--template", defaultTemplate, "--app-id", appID) + return base } // parseRepoURLFromEnvelope extracts data.repository_url from a lark-cli JSON @@ -533,7 +539,8 @@ func appsInitExecute(ctx context.Context, rctx *common.RuntimeContext) error { initLogf(rctx, "Initializing app code (running miaoda-cli)...") explicitTemplate := strings.TrimSpace(rctx.Str("template")) - scaffold, err := runScaffold(ctx, dir, appID, appType, explicitTemplate) + sourcePath := strings.TrimSpace(rctx.Str("source-path")) + scaffold, err := runScaffold(ctx, dir, appID, appType, explicitTemplate, sourcePath) if err != nil { return err } diff --git a/shortcuts/apps/apps_init_test.go b/shortcuts/apps/apps_init_test.go index 227e6e123..fc9774430 100644 --- a/shortcuts/apps/apps_init_test.go +++ b/shortcuts/apps/apps_init_test.go @@ -261,7 +261,7 @@ func TestRunScaffold_EmptyRepo(t *testing.T) { t.Run("ls="+ls, func(t *testing.T) { f := &fakeCommandRunner{results: map[string]fakeCallResult{"git ls-files": {stdout: ls}}} withFakeRunner(t, f) - kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "nestjs-react-fullstack") + kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "nestjs-react-fullstack", "") if err != nil || kind != "init" { t.Fatalf("ls=%q kind=%q err=%v, want init", ls, kind, err) } @@ -280,7 +280,7 @@ func TestRunScaffold_NonEmpty_SyncsWhenNoSteering(t *testing.T) { dir := t.TempDir() // no steering dir, no meta.json f := &fakeCommandRunner{results: map[string]fakeCallResult{"git ls-files": {stdout: "src/x.ts\n"}}} withFakeRunner(t, f) - kind, err := runScaffold(context.Background(), dir, "app_x", "", "nestjs-react-fullstack") + kind, err := runScaffold(context.Background(), dir, "app_x", "", "nestjs-react-fullstack", "") if err != nil || kind != "upgrade" { t.Fatalf("kind=%q err=%v, want upgrade", kind, err) } @@ -299,7 +299,7 @@ func TestRunScaffold_NonEmpty_SkipsSyncWhenSteeringExists(t *testing.T) { os.MkdirAll(filepath.Join(dir, steeringRelPath), 0o755) f := &fakeCommandRunner{results: map[string]fakeCallResult{"git ls-files": {stdout: "src/x.ts\n"}}} withFakeRunner(t, f) - if _, err := runScaffold(context.Background(), dir, "app_x", "", "nestjs-react-fullstack"); err != nil { + if _, err := runScaffold(context.Background(), dir, "app_x", "", "nestjs-react-fullstack", ""); err != nil { t.Fatal(err) } if findCallArg(f.calls, "npx", "skills", "sync") != nil { @@ -313,7 +313,7 @@ func TestRunScaffold_AppInitFailure(t *testing.T) { "npx -y": {stderr: "boom", err: errors.New("exit 1")}, }} withFakeRunner(t, f) - if _, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "nestjs-react-fullstack"); err == nil { + if _, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "nestjs-react-fullstack", ""); err == nil { t.Error("app init failure must propagate") } } @@ -1250,7 +1250,7 @@ func TestRunScaffold_NonEmpty_SyncFailure(t *testing.T) { "git ls-files": {stdout: "src/x.ts\n"}, "npx -y": {err: errors.New("sync boom")}, }}) - if _, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "tpl"); err == nil { + if _, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "tpl", ""); err == nil { t.Error("npx app sync failure must surface as an error") } } @@ -1630,7 +1630,7 @@ func TestRunScaffold_SubprocessFailureIsExternalTool(t *testing.T) { "git ls-files": {stderr: "fatal: not a git repository", err: cause}, }} withFakeRunner(t, f) - _, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "nestjs-react-fullstack") + _, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "nestjs-react-fullstack", "") if err == nil { t.Fatalf("expected error from failing git subprocess") } @@ -1649,7 +1649,7 @@ func TestRunScaffold_SubprocessFailureIsExternalTool(t *testing.T) { func TestRunScaffold_HtmlPassesTemplate(t *testing.T) { f := &fakeCommandRunner{results: map[string]fakeCallResult{"git ls-files": {stdout: ""}}} withFakeRunner(t, f) - kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "html", "") + kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "html", "", "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1668,7 +1668,7 @@ func TestRunScaffold_HtmlPassesTemplate(t *testing.T) { func TestRunScaffold_ModernHtmlPassesTemplate(t *testing.T) { f := &fakeCommandRunner{results: map[string]fakeCallResult{"git ls-files": {stdout: ""}}} withFakeRunner(t, f) - kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "modern_html", "") + kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "modern_html", "", "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1687,7 +1687,7 @@ func TestRunScaffold_ModernHtmlPassesTemplate(t *testing.T) { func TestRunScaffold_EmptyAppTypeFallback(t *testing.T) { f := &fakeCommandRunner{results: map[string]fakeCallResult{"git ls-files": {stdout: ""}}} withFakeRunner(t, f) - kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "") + kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "", "", "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1706,7 +1706,7 @@ func TestRunScaffold_EmptyAppTypeFallback(t *testing.T) { func TestRunScaffold_ExplicitTemplateOverride(t *testing.T) { f := &fakeCommandRunner{results: map[string]fakeCallResult{"git ls-files": {stdout: ""}}} withFakeRunner(t, f) - kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "modern_html", "custom-template") + kind, err := runScaffold(context.Background(), t.TempDir(), "app_x", "modern_html", "custom-template", "") if err != nil { t.Fatalf("unexpected error: %v", err) }