cli: -app resolves on every app-scoped member verb
Sweep by precise brief (Sonnet), plus: bare no-session invocations of API-only verbs now stop on the REAL blocker (no deployment target / login offer) instead of a usage line for a flag that is no longer required — the four pinned tests move to the new contract; ship's capability block validates against the resolved name when -app was omitted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
13 files changed,
+308
−191
appconfig_cli.go+88 −47cli.go+115 −57deploy.go+6 −4domains_cli.go+11 −9domains_cli_test.go+7 −3instances_cli.go+21 −15logs.go+3 −5restart.go+9 −8restart_test.go+6 −2retention.go+5 −5retention_cli_test.go+5 −2store.go+24 −20store_test.go+8 −14
diff --git a/appconfig_cli.go b/appconfig_cli.go| index e9b382c98..4dc3facc0 100644 |
| --- a/appconfig_cli.go |
| +++ b/appconfig_cli.go |
| @@ -196,9 +196,7 @@ func runEnvSet(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos env set -app <a> [-environment <name>] [-console <origin>] KEY=value [KEY2=value2 ...]") |
| - } |
| + usage := "usage: carlos env set -app <a> [-environment <name>] [-console <origin>] KEY=value [KEY2=value2 ...]" |
| checkEnvironmentFlag(*environment) |
| kv, err := parseKV(fs.Args()) |
| if err != nil { |
| @@ -211,18 +209,23 @@ func runEnvSet(args []string) { |
| } |
| if sess != nil { |
| printUsing(os.Stdout, sess) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| set := make(map[string]appconfig.Var, len(kv)) |
| for k, v := range kv { |
| set[k] = appconfig.Var{Kind: "env", Value: v} |
| } |
| - if _, err := sess.configApply(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, |
| + if _, err := sess.configApply(context.Background(), acct, app2, |
| apiConfigApply{Environment: *environment, Set: set}); err != nil { |
| log.Fatal(err) |
| } |
| - fmt.Printf("%s %d env var(s) for %s (%s)\n", colorFor(os.Stdout).ok("set"), len(kv), *app, bundleLabel(*environment)) |
| + fmt.Printf("%s %d env var(s) for %s (%s)\n", colorFor(os.Stdout).ok("set"), len(kv), app2, bundleLabel(*environment)) |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| if err := saveEnvVars(context.Background(), openStore(), acctOf(), *app, *environment, kv); err != nil { |
| log.Fatal(err) |
| } |
| @@ -236,8 +239,9 @@ func runEnvUnset(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() == 0 { |
| - log.Fatal("usage: carlos env unset -app <a> [-environment <name>] [-console <origin>] KEY [KEY2 ...]") |
| + usage := "usage: carlos env unset -app <a> [-environment <name>] [-console <origin>] KEY [KEY2 ...]" |
| + if fs.NArg() == 0 { |
| + log.Fatal(usage) |
| } |
| checkEnvironmentFlag(*environment) |
| @@ -247,15 +251,20 @@ func runEnvUnset(args []string) { |
| } |
| if sess != nil { |
| printUsing(os.Stdout, sess) |
| - removed, err := sess.configApply(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + removed, err := sess.configApply(context.Background(), acct, app2, |
| apiConfigApply{Environment: *environment, Unset: fs.Args()}) |
| if err != nil { |
| log.Fatal(err) |
| } |
| - fmt.Printf("%s %d of %d var(s) for %s (%s)\n", colorFor(os.Stdout).ok("unset"), removed, fs.NArg(), *app, bundleLabel(*environment)) |
| + fmt.Printf("%s %d of %d var(s) for %s (%s)\n", colorFor(os.Stdout).ok("unset"), removed, fs.NArg(), app2, bundleLabel(*environment)) |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| removed, err := unsetVars(context.Background(), openStore(), acctOf(), *app, *environment, fs.Args()) |
| if err != nil { |
| log.Fatal(err) |
| @@ -270,9 +279,7 @@ func runEnvList(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos env list -app <a> [-environment <name>] [-console <origin>]") |
| - } |
| + usage := "usage: carlos env list -app <a> [-environment <name>] [-console <origin>]" |
| checkEnvironmentFlag(*environment) |
| sess, err := resolveMode(*consoleFlag) |
| @@ -281,14 +288,19 @@ func runEnvList(args []string) { |
| } |
| if sess != nil { |
| printUsing(os.Stdout, sess) |
| - cfg, err := sess.configGet(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, *environment) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + cfg, err := sess.configGet(context.Background(), acct, app2, *environment) |
| if err != nil { |
| log.Fatal(err) |
| } |
| - printEnvList(*app, *environment, cfg.ExecDelivery, envVarsFromAPI(cfg.Vars)) |
| + printEnvList(app2, *environment, cfg.ExecDelivery, envVarsFromAPI(cfg.Vars)) |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| m, err := loadBundle(context.Background(), openStore(), acctOf(), *app, *environment) |
| if err != nil { |
| log.Fatal(err) |
| @@ -362,8 +374,9 @@ func runEnvExecDelivery(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() != 1 { |
| - log.Fatal("usage: carlos env exec-delivery -app <a> [-console <origin>] file|off") |
| + usage := "usage: carlos env exec-delivery -app <a> [-console <origin>] file|off" |
| + if fs.NArg() != 1 { |
| + log.Fatal(usage) |
| } |
| if *environment != "" { |
| log.Fatal(`carlos env exec-delivery: -environment is refused — exec_delivery is APP-LEVEL ONLY, read from the ` + |
| @@ -387,8 +400,11 @@ func runEnvExecDelivery(args []string) { |
| if err != nil { |
| log.Fatal(err) |
| } |
| + appName := *app |
| if sess != nil { |
| printUsing(os.Stdout, sess) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + appName = resolvedApp(sess, acct, *app, usage) |
| // The wire vocabulary is "file"/"off", never "" — value is "" for |
| // the local SetExecDelivery convention (clearing the field), but |
| // the console's PUT decodes ExecDelivery as a pointer where nil |
| @@ -397,17 +413,22 @@ func runEnvExecDelivery(args []string) { |
| if value != "" { |
| wire = value |
| } |
| - if _, err := sess.configApply(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, |
| + if _, err := sess.configApply(context.Background(), acct, appName, |
| apiConfigApply{ExecDelivery: &wire}); err != nil { |
| log.Fatal(err) |
| } |
| - } else if err := appconfig.SetExecDelivery(context.Background(), openStore(), acctOf(), *app, value); err != nil { |
| - log.Fatal(err) |
| + } else { |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| + if err := appconfig.SetExecDelivery(context.Background(), openStore(), acctOf(), *app, value); err != nil { |
| + log.Fatal(err) |
| + } |
| } |
| if value == "" { |
| - fmt.Printf("%s off for %s\n", colorFor(os.Stdout).ok("exec-delivery"), *app) |
| + fmt.Printf("%s off for %s\n", colorFor(os.Stdout).ok("exec-delivery"), appName) |
| } else { |
| - fmt.Printf("%s %s for %s\n", colorFor(os.Stdout).ok("exec-delivery"), value, *app) |
| + fmt.Printf("%s %s for %s\n", colorFor(os.Stdout).ok("exec-delivery"), value, appName) |
| } |
| } |
| @@ -417,9 +438,7 @@ func runEnvEnvironments(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos env environments -app <a> [-console <origin>]") |
| - } |
| + usage := "usage: carlos env environments -app <a> [-console <origin>]" |
| sess, err := resolveMode(*consoleFlag) |
| if err != nil { |
| @@ -427,7 +446,9 @@ func runEnvEnvironments(args []string) { |
| } |
| if sess != nil { |
| printUsing(os.Stdout, sess) |
| - cfg, err := sess.configGet(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, "") |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + cfg, err := sess.configGet(context.Background(), acct, app2, "") |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -437,6 +458,9 @@ func runEnvEnvironments(args []string) { |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| names, err := appconfig.ListEnvironments(context.Background(), openStore(), acctOf(), *app) |
| if err != nil { |
| log.Fatal(err) |
| @@ -467,25 +491,31 @@ func runEnvSync(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos env sync -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]") |
| - } |
| + usage := "usage: carlos env sync -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| sess, err := resolveMode(*consoleFlag) |
| if err != nil { |
| log.Fatal(err) |
| } |
| + appName := *app |
| if sess != nil { |
| printUsing(os.Stdout, sess) |
| - if err := sess.configSync(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app); err != nil { |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + appName = resolvedApp(sess, acct, *app, usage) |
| + if err := sess.configSync(context.Background(), acct, appName); err != nil { |
| + log.Fatal(err) |
| + } |
| + } else { |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| + if err := appconfig.TouchGeneration(context.Background(), openStore(), acctOf(), *app); err != nil { |
| log.Fatal(err) |
| } |
| - } else if err := appconfig.TouchGeneration(context.Background(), openStore(), acctOf(), *app); err != nil { |
| - log.Fatal(err) |
| } |
| fmt.Printf("%s for %s — exec-backed instances converge within seconds; "+ |
| "unit-backed routes within seconds where the box's carlos-adopt.path trigger is armed, "+ |
| - "else on the next hourly root pass\n", colorFor(os.Stdout).ok("config sync requested"), *app) |
| + "else on the next hourly root pass\n", colorFor(os.Stdout).ok("config sync requested"), appName) |
| } |
| const secretsUsage = `usage: carlos secrets <command> |
| @@ -583,9 +613,7 @@ func runSecretsSet(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos secrets set -app <a> [-environment <name>] [-console <origin>] KEY=value [KEY2=value2 ...]") |
| - } |
| + usage := "usage: carlos secrets set -app <a> [-environment <name>] [-console <origin>] KEY=value [KEY2=value2 ...]" |
| checkEnvironmentFlag(*environment) |
| kv, err := parseKV(fs.Args()) |
| if err != nil { |
| @@ -614,7 +642,8 @@ func runSecretsSet(args []string) { |
| // writing it into another's config — an undecryptable secret whose |
| // only symptom is a render failure in the hourly adopt log. |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| - pub := secretsPubKeyAPI(sess, acct, *app) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + pub := secretsPubKeyAPI(sess, acct, app2) |
| set := make(map[string]appconfig.Var, len(kv)) |
| for k, v := range kv { |
| env, err := appconfig.Seal(pub, []byte(v)) |
| @@ -623,14 +652,17 @@ func runSecretsSet(args []string) { |
| } |
| set[k] = appconfig.Var{Kind: "secret", Envelope: env} |
| } |
| - if _, err := sess.configApply(context.Background(), acct, *app, |
| + if _, err := sess.configApply(context.Background(), acct, app2, |
| apiConfigApply{Environment: *environment, Set: set}); err != nil { |
| log.Fatal(err) |
| } |
| - fmt.Printf("%s %d secret(s) for %s (%s)\n", colorFor(os.Stdout).ok("set"), len(kv), *app, bundleLabel(*environment)) |
| + fmt.Printf("%s %d secret(s) for %s (%s)\n", colorFor(os.Stdout).ok("set"), len(kv), app2, bundleLabel(*environment)) |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| pub := secretsPubKey() |
| sealed := make(map[string]string, len(kv)) |
| for k, v := range kv { |
| @@ -653,8 +685,9 @@ func runSecretsUnset(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() == 0 { |
| - log.Fatal("usage: carlos secrets unset -app <a> [-environment <name>] [-console <origin>] KEY [KEY2 ...]") |
| + usage := "usage: carlos secrets unset -app <a> [-environment <name>] [-console <origin>] KEY [KEY2 ...]" |
| + if fs.NArg() == 0 { |
| + log.Fatal(usage) |
| } |
| checkEnvironmentFlag(*environment) |
| @@ -664,15 +697,20 @@ func runSecretsUnset(args []string) { |
| } |
| if sess != nil { |
| printUsing(os.Stdout, sess) |
| - removed, err := sess.configApply(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + removed, err := sess.configApply(context.Background(), acct, app2, |
| apiConfigApply{Environment: *environment, Unset: fs.Args()}) |
| if err != nil { |
| log.Fatal(err) |
| } |
| - fmt.Printf("%s %d of %d secret(s) for %s (%s)\n", colorFor(os.Stdout).ok("unset"), removed, fs.NArg(), *app, bundleLabel(*environment)) |
| + fmt.Printf("%s %d of %d secret(s) for %s (%s)\n", colorFor(os.Stdout).ok("unset"), removed, fs.NArg(), app2, bundleLabel(*environment)) |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| removed, err := unsetVars(context.Background(), openStore(), acctOf(), *app, *environment, fs.Args()) |
| if err != nil { |
| log.Fatal(err) |
| @@ -699,9 +737,7 @@ func runSecretsList(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos secrets list -app <a> [-environment <name>] [-console <origin>]") |
| - } |
| + usage := "usage: carlos secrets list -app <a> [-environment <name>] [-console <origin>]" |
| checkEnvironmentFlag(*environment) |
| sess, err := resolveMode(*consoleFlag) |
| @@ -710,7 +746,9 @@ func runSecretsList(args []string) { |
| } |
| if sess != nil { |
| printUsing(os.Stdout, sess) |
| - cfg, err := sess.configGet(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, *environment) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + cfg, err := sess.configGet(context.Background(), acct, app2, *environment) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -720,10 +758,13 @@ func runSecretsList(args []string) { |
| keys = append(keys, v.Key) |
| } |
| } |
| - printSecretsList(*app, *environment, keys) |
| + printSecretsList(app2, *environment, keys) |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| m, err := loadBundle(context.Background(), openStore(), acctOf(), *app, *environment) |
| if err != nil { |
| log.Fatal(err) |
diff --git a/cli.go b/cli.go| index 8963af6d5..482a64298 100644 |
| --- a/cli.go |
| +++ b/cli.go |
| @@ -1710,8 +1710,9 @@ func runShip(args []string) { |
| // the target (release versioning spec, 2026-08-08). Everything else about |
| // this check is unchanged — an app and at least one file are still the |
| // minimum a ship can mean. |
| - if *app == "" || fs.NArg() == 0 { |
| - log.Fatal("usage: carlos ship -app <a> [-version <sha>] [-label <one line>] [-notes <text> | -notes-file <file>] [-kind binary|static] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] [-capabilities <file.json>] <files-or-site-dir>") |
| + usage := "usage: carlos ship -app <a> [-version <sha>] [-label <one line>] [-notes <text> | -notes-file <file>] [-kind binary|static] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] [-capabilities <file.json>] <files-or-site-dir>" |
| + if fs.NArg() == 0 { |
| + log.Fatal(usage) |
| } |
| // -notes and -notes-file are two spellings of ONE manifest field, so both |
| @@ -1743,9 +1744,18 @@ func runShip(args []string) { |
| // for anything semantically wrong. Direct mode would find out again in |
| // release.Ship and API mode again in the console, but neither of those is |
| // where a human is standing. |
| - capsSet, err := readCapabilities(*capsFile, *app) |
| - if err != nil { |
| - log.Fatal(err) |
| + // Validated against the app NAME, which may still be empty here when |
| + // -app is left to resolution — so with an omitted -app the read happens |
| + // again after resolution (below), and this pre-split pass only runs |
| + // when the name is already known. Both doors still refuse the same |
| + // bytes with the same message; an omitted -app just moves the moment. |
| + var capsSet *caps.Set |
| + if *app != "" { |
| + var err error |
| + capsSet, err = readCapabilities(*capsFile, *app) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| } |
| sess, err := resolveMode(*consoleFlag) |
| @@ -1765,12 +1775,24 @@ func runShip(args []string) { |
| // runShipAPI's body has registered a defer at all. |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| - if err := runShipAPI(sess, acct, fs, *app, *ver, *kind, *target, *label, noteText, capsSet, false); err != nil { |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + if *app == "" { |
| + // -app came from resolution: the capability block is validated |
| + // now, against the real name. |
| + capsSet, err = readCapabilities(*capsFile, app2) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + } |
| + if err := runShipAPI(sess, acct, fs, app2, *ver, *kind, *target, *label, noteText, capsSet, false); err != nil { |
| log.Fatal(err) |
| } |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| files := map[string][]byte{} |
| arts := map[string]release.Artifact{} |
| addFile := func(name string, data []byte) { |
| @@ -2010,8 +2032,9 @@ func runPromote(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to promote through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() != 2 { |
| - log.Fatal("usage: carlos promote -app <a> [-hotfix] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] <version> <channel>") |
| + usage := "usage: carlos promote -app <a> [-hotfix] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] <version> <channel>" |
| + if fs.NArg() != 2 { |
| + log.Fatal(usage) |
| } |
| version, channel := fs.Arg(0), fs.Arg(1) |
| @@ -2031,18 +2054,22 @@ func runPromote(args []string) { |
| } |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| - res, err := sess.promote(context.Background(), acct, *app, version, channel) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + res, err := sess.promote(context.Background(), acct, app2, version, channel) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if res.NoOp { |
| - mutedf(stdout, "%s %s is already on %s — nothing to do\n", *app, version, channel) |
| + mutedf(stdout, "%s %s is already on %s — nothing to do\n", app2, version, channel) |
| } else { |
| - fmt.Fprintf(stdout, "%s %s %s -> %s\n", colorFor(stdout).ok("promoted"), *app, version, channel) |
| + fmt.Fprintf(stdout, "%s %s %s -> %s\n", colorFor(stdout).ok("promoted"), app2, version, channel) |
| } |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| res, err := releaseSigner().PromoteOnLadder(context.Background(), openStore(), |
| acctOf(), *app, channel, version, |
| release.PromoteOptions{Hotfix: *hotfix, Actor: promoteActor()}) |
| @@ -2096,9 +2123,7 @@ func runChannels(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos channels -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]") |
| - } |
| + usage := "usage: carlos channels -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| sess, err := resolveMode(*consoleFlag) |
| if err != nil { |
| @@ -2107,7 +2132,8 @@ func runChannels(args []string) { |
| if sess != nil { |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| - chans, err := sess.channels(context.Background(), acct, *app) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + chans, err := sess.channels(context.Background(), acct, app2) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -2117,7 +2143,7 @@ func runChannels(args []string) { |
| // fails degrades to a tag-less table — the question this verb answers |
| // is "what is each channel serving", and no tag has ever been part of |
| // that answer. |
| - tags, err := sess.tags(context.Background(), acct, *app) |
| + tags, err := sess.tags(context.Background(), acct, app2) |
| if err != nil { |
| mutedf(stdout, "(tags unreadable: %v)\n", err) |
| } |
| @@ -2138,6 +2164,9 @@ func runChannels(args []string) { |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| acct := acctOf() |
| s := openStore() |
| prefix := release.ChannelsPrefix(acct, *app) |
| @@ -2185,16 +2214,9 @@ func runVersionTarget(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read or set through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() > 1 { |
| - log.Fatal("usage: carlos version target -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] [<semver>]") |
| - } |
| - |
| - // unset is what both modes print when no target has ever been set, and it |
| - // names the way to set one: an operator meeting this line is one command |
| - // away from the feature, and the command is not guessable from "none". |
| - unset := func() { |
| - mutedf(stdout, "no version target set\n") |
| - mutedf(stdout, " set one with: carlos version target -app %s 0.1.0\n", *app) |
| + usage := "usage: carlos version target -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] [<semver>]" |
| + if fs.NArg() > 1 { |
| + log.Fatal(usage) |
| } |
| sess, err := resolveMode(*consoleFlag) |
| @@ -2204,8 +2226,17 @@ func runVersionTarget(args []string) { |
| if sess != nil { |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + // unset is what both modes print when no target has ever been set, and |
| + // it names the way to set one: an operator meeting this line is one |
| + // command away from the feature, and the command is not guessable |
| + // from "none". |
| + unset := func() { |
| + mutedf(stdout, "no version target set\n") |
| + mutedf(stdout, " set one with: carlos version target -app %s 0.1.0\n", app2) |
| + } |
| if fs.NArg() == 0 { |
| - vt, err := sess.versionTarget(context.Background(), acct, *app) |
| + vt, err := sess.versionTarget(context.Background(), acct, app2) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -2219,7 +2250,7 @@ func runVersionTarget(args []string) { |
| } |
| return |
| } |
| - stored, err := sess.setVersionTarget(context.Background(), acct, *app, fs.Arg(0)) |
| + stored, err := sess.setVersionTarget(context.Background(), acct, app2, fs.Arg(0)) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -2227,6 +2258,13 @@ func runVersionTarget(args []string) { |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| + unset := func() { |
| + mutedf(stdout, "no version target set\n") |
| + mutedf(stdout, " set one with: carlos version target -app %s 0.1.0\n", *app) |
| + } |
| acct := acctOf() |
| s := openStore() |
| if fs.NArg() == 0 { |
| @@ -2337,19 +2375,19 @@ func runReleases(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos releases -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]") |
| - } |
| + usage := "usage: carlos releases -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| sess, err := resolveMode(*consoleFlag) |
| if err != nil { |
| log.Fatal(err) |
| } |
| var rows []releaseRow |
| + appName := *app |
| if sess != nil { |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| - rels, err := sess.releases(context.Background(), acct, *app) |
| + appName = resolvedApp(sess, acct, *app, usage) |
| + rels, err := sess.releases(context.Background(), acct, appName) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -2357,14 +2395,17 @@ func runReleases(args []string) { |
| rows = append(rows, releaseRow{Version: r.Version, Label: r.Label, Kind: r.Kind, BuiltAt: r.BuiltAt, Tag: r.Tag}) |
| } |
| } else { |
| - rows, err = localReleases(context.Background(), openStore(), acctOf(), *app) |
| + if appName == "" { |
| + log.Fatal(usage) |
| + } |
| + rows, err = localReleases(context.Background(), openStore(), acctOf(), appName) |
| if err != nil { |
| log.Fatal(err) |
| } |
| } |
| if len(rows) == 0 { |
| - mutedf(stdout, "no releases yet — carlos ship -app %s <files>\n", *app) |
| + mutedf(stdout, "no releases yet — carlos ship -app %s <files>\n", appName) |
| return |
| } |
| w := newTable(stdout) |
| @@ -2385,8 +2426,9 @@ func runRollback(args []string) { |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to roll back through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() != 1 { |
| - log.Fatal("usage: carlos rollback -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] <channel>") |
| + usage := "usage: carlos rollback -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] <channel>" |
| + if fs.NArg() != 1 { |
| + log.Fatal(usage) |
| } |
| sess, err := resolveMode(*consoleFlag) |
| @@ -2396,13 +2438,17 @@ func runRollback(args []string) { |
| if sess != nil { |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| - if err := sess.rollback(context.Background(), acct, *app, fs.Arg(0)); err != nil { |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + if err := sess.rollback(context.Background(), acct, app2, fs.Arg(0)); err != nil { |
| log.Fatal(err) |
| } |
| - fmt.Fprintf(stdout, "%s %s %s\n", colorFor(stdout).ok("rolled back"), *app, fs.Arg(0)) |
| + fmt.Fprintf(stdout, "%s %s %s\n", colorFor(stdout).ok("rolled back"), app2, fs.Arg(0)) |
| return |
| } |
| + if *app == "" { |
| + log.Fatal(usage) |
| + } |
| if err := releaseSigner().Rollback(context.Background(), openStore(), acctOf(), *app, fs.Arg(0)); err != nil { |
| log.Fatal(err) |
| } |
| @@ -2525,8 +2571,9 @@ func runLedgerAppend(args []string) { |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to append through (default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() != 2 { |
| - log.Fatal("usage: carlos ledger append -app <a> [-account <acct>] [-account-id <sqid>] <ledger> <entry.json|->") |
| + usage := "usage: carlos ledger append -app <a> [-account <acct>] [-account-id <sqid>] <ledger> <entry.json|->" |
| + if fs.NArg() != 2 { |
| + log.Fatal(usage) |
| } |
| name, src := fs.Arg(0), fs.Arg(1) |
| if !ledger.ValidName(name) { |
| @@ -2552,7 +2599,9 @@ func runLedgerAppend(args []string) { |
| sess := ledgerAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| - a, err := sess.ledgerAppend(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, name, json.RawMessage(raw)) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + a, err := sess.ledgerAppend(context.Background(), acct, app2, name, json.RawMessage(raw)) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -2569,8 +2618,9 @@ func runLedgerHead(args []string) { |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() != 1 { |
| - log.Fatal("usage: carlos ledger head -app <a> [-account <acct>] [-account-id <sqid>] <ledger>") |
| + usage := "usage: carlos ledger head -app <a> [-account <acct>] [-account-id <sqid>] <ledger>" |
| + if fs.NArg() != 1 { |
| + log.Fatal(usage) |
| } |
| name := fs.Arg(0) |
| if !ledger.ValidName(name) { |
| @@ -2578,7 +2628,9 @@ func runLedgerHead(args []string) { |
| } |
| sess := ledgerAPISession(*consoleFlag) |
| - h, err := sess.ledgerHead(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, name) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + h, err := sess.ledgerHead(context.Background(), acct, app2, name) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -2595,13 +2647,13 @@ func runLedgerList(args []string) { |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal("usage: carlos ledger list -app <a> [-account <acct>] [-account-id <sqid>]") |
| - } |
| + usage := "usage: carlos ledger list -app <a> [-account <acct>] [-account-id <sqid>]" |
| sess := ledgerAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| - body, err := sess.ledgerList(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| + body, err := sess.ledgerList(context.Background(), acct, app2) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -2619,7 +2671,7 @@ func runLedgerList(args []string) { |
| case res.Manifest.Public: |
| title := res.Manifest.Title |
| if title == "" { |
| - title = *app |
| + title = app2 |
| } |
| fmt.Fprintf(stdout, "published %q: %s\n", title, strings.Join(res.Manifest.Ledgers, ", ")) |
| default: |
| @@ -2645,8 +2697,9 @@ func runLedgerPublish(args []string) { |
| title := fs.String("title", "", "display title for the published set") |
| consoleFlag := fs.String("console", "", "console origin to publish through (default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() == 0 { |
| - log.Fatal("usage: carlos ledger publish -app <a> [-account <acct>] [-account-id <sqid>] [-public=true|false] [-title <t>] <ledger>...") |
| + usage := "usage: carlos ledger publish -app <a> [-account <acct>] [-account-id <sqid>] [-public=true|false] [-title <t>] <ledger>..." |
| + if fs.NArg() == 0 { |
| + log.Fatal(usage) |
| } |
| names := fs.Args() |
| for _, n := range names { |
| @@ -2657,8 +2710,10 @@ func runLedgerPublish(args []string) { |
| sess := ledgerAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| m := ledger.Manifest{Public: *public, Ledgers: names, Title: *title} |
| - got, err := sess.ledgerPublish(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, m) |
| + got, err := sess.ledgerPublish(context.Background(), acct, app2, m) |
| if err != nil { |
| log.Fatal(err) |
| } |
| @@ -2673,10 +2728,10 @@ func runLedgerPublish(args []string) { |
| verb = "unpublished" |
| } |
| if len(got.Ledgers) == 0 { |
| - fmt.Fprintf(stdout, "%s %s: no public ledgers\n", verb, *app) |
| + fmt.Fprintf(stdout, "%s %s: no public ledgers\n", verb, app2) |
| return |
| } |
| - fmt.Fprintf(stdout, "%s %s: public ledgers now [%s]\n", verb, *app, strings.Join(got.Ledgers, " ")) |
| + fmt.Fprintf(stdout, "%s %s: public ledgers now [%s]\n", verb, app2, strings.Join(got.Ledgers, " ")) |
| } |
| func runLedgerBlob(args []string) { |
| @@ -2685,8 +2740,9 @@ func runLedgerBlob(args []string) { |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to upload through (default: the only logged-in one)") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() < 2 { |
| - log.Fatal("usage: carlos ledger blob -app <a> [-account <acct>] [-account-id <sqid>] <ledger> <file>...") |
| + usage := "usage: carlos ledger blob -app <a> [-account <acct>] [-account-id <sqid>] <ledger> <file>..." |
| + if fs.NArg() < 2 { |
| + log.Fatal(usage) |
| } |
| name := fs.Arg(0) |
| if !ledger.ValidName(name) { |
| @@ -2696,6 +2752,8 @@ func runLedgerBlob(args []string) { |
| sess := ledgerAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, usage) |
| for _, path := range files { |
| data, err := os.ReadFile(path) |
| if err != nil { |
| @@ -2704,7 +2762,7 @@ func runLedgerBlob(args []string) { |
| if len(data) > ledger.MaxBlob { |
| log.Fatalf("ledger blob: %s is %d bytes, exceeds the %d byte cap", path, len(data), ledger.MaxBlob) |
| } |
| - sha, err := sess.ledgerBlob(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, name, data) |
| + sha, err := sess.ledgerBlob(context.Background(), acct, app2, name, data) |
| if err != nil { |
| log.Fatal(err) |
| } |
diff --git a/deploy.go b/deploy.go| index aea3d8402..8dc81c995 100644 |
| --- a/deploy.go |
| +++ b/deploy.go |
| @@ -41,10 +41,11 @@ func runDeploy(args []string) { |
| hostFlag := fs.String("host", "", "scope channel resolution and the watch to one instance host (required for an app with no instances, e.g. a static site)") |
| timeout := fs.Duration("timeout", defaultDeployTimeout, "how long to watch the URL before failing") |
| fs.Parse(args) |
| - if *app == "" || fs.NArg() == 0 { |
| - log.Fatal("usage: carlos deploy -app <a> [-version <sha>] [-kind binary|static] [-target linux-arm64] " + |
| - "[-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] [-capabilities <file.json>] " + |
| - "[-channel <ch>] [-host <host>] [-timeout 120s] <files-or-site-dir>") |
| + usage := "usage: carlos deploy -app <a> [-version <sha>] [-kind binary|static] [-target linux-arm64] " + |
| + "[-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] [-capabilities <file.json>] " + |
| + "[-channel <ch>] [-host <host>] [-timeout 120s] <files-or-site-dir>" |
| + if fs.NArg() == 0 { |
| + log.Fatal(usage) |
| } |
| if *ver == "" { |
| *ver = gitShortHead() |
| @@ -61,6 +62,7 @@ func runDeploy(args []string) { |
| sess := requireDeployAPISession(*consoleFlag) // API-mode only: member verb |
| printUsing(stdout, sess) // printed HERE, once: runShipAPI's own banner is suppressed below |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + *app = resolvedApp(sess, acct, *app, usage) |
| ctx := context.Background() |
| recs, _, _, err := sess.instanceList(ctx, acct, *app, "", "") |
| if err != nil { |
diff --git a/domains_cli.go b/domains_cli.go| index b046ab640..5583743a8 100644 |
| --- a/domains_cli.go |
| +++ b/domains_cli.go |
| @@ -80,7 +80,7 @@ func runDomainsAttach(args []string) { |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| fs.Parse(args) |
| host := strings.TrimSpace(fs.Arg(0)) |
| - if *app == "" || host == "" || fs.NArg() > 1 { |
| + if host == "" || fs.NArg() > 1 { |
| log.Fatal(domainsAttachUsage) |
| } |
| // -wildcard/-catchall are tri-state, the same reason apps place's |
| @@ -103,8 +103,9 @@ func runDomainsAttach(args []string) { |
| sess := requireAPISession("attach", *consoleFlag) |
| printUsing(stdout, sess) |
| acct := apiAccount(acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, domainsAttachUsage) |
| ctx := context.Background() |
| - created, resolvedRoute, err := sess.domainAttach(ctx, acct, *app, host, strings.TrimSpace(*route), "", "", wildcardArg, catchallArg) |
| + created, resolvedRoute, err := sess.domainAttach(ctx, acct, app2, host, strings.TrimSpace(*route), "", "", wildcardArg, catchallArg) |
| if err != nil { |
| log.Fatalf("domains attach: %v", err) |
| } |
| @@ -113,7 +114,7 @@ func runDomainsAttach(args []string) { |
| } else { |
| fmt.Fprintf(stdout, "%s %s -> %s\n", colorFor(stdout).ok("already attached"), host, resolvedRoute) |
| } |
| - printDomainDNSLine(ctx, sess, acct, *app, host) |
| + printDomainDNSLine(ctx, sess, acct, app2, host) |
| } |
| // printDomainDNSLine completes the job `carlos domains attach` promises |
| @@ -160,12 +161,14 @@ func runDomainsDetach(args []string) { |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| fs.Parse(args) |
| host := strings.TrimSpace(fs.Arg(0)) |
| - if *app == "" || host == "" || fs.NArg() > 1 { |
| + if host == "" || fs.NArg() > 1 { |
| log.Fatal(domainsDetachUsage) |
| } |
| sess := requireAPISession("detach", *consoleFlag) |
| printUsing(stdout, sess) |
| - removed, err := sess.domainDetach(context.Background(), apiAccount(acctRaw, acctIDRaw), *app, host) |
| + acct := apiAccount(acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, domainsDetachUsage) |
| + removed, err := sess.domainDetach(context.Background(), acct, app2, host) |
| if err != nil { |
| log.Fatalf("domains detach: %v", err) |
| } |
| @@ -183,12 +186,11 @@ func runDomainsList(args []string) { |
| acctRaw := fs.String("account", "ops", "account (sqid or name) the app belongs to") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal(domainsListUsage) |
| - } |
| sess := requireAPISession("list", *consoleFlag) |
| printUsing(stdout, sess) |
| - recs, err := sess.domainList(context.Background(), apiAccount(acctRaw, acctIDRaw), *app) |
| + acct := apiAccount(acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, domainsListUsage) |
| + recs, err := sess.domainList(context.Background(), acct, app2) |
| if err != nil { |
| log.Fatalf("domains list: %v", err) |
| } |
diff --git a/domains_cli_test.go b/domains_cli_test.go| index 693e88b3a..b9ea8f14b 100644 |
| --- a/domains_cli_test.go |
| +++ b/domains_cli_test.go |
| @@ -543,11 +543,15 @@ func TestDomainsCLIUsageErrors(t *testing.T) { |
| }{ |
| {"domains no args", []string{"domains"}, "usage: carlos domains"}, |
| {"domains bogus verb", []string{"domains", "bogus"}, "usage: carlos domains"}, |
| - {"attach no app", []string{"domains", "attach", "tickets.example.com"}, "usage: carlos domains attach"}, |
| + // The no-app cases stop on the missing SESSION since the |
| + // project-defaults spec (2026-08-13): -app resolves once a session |
| + // exists, so with nothing configured the real blocker is named — |
| + // not a usage line for a flag that is no longer required. |
| + {"attach no app", []string{"domains", "attach", "tickets.example.com"}, "no deployment target configured"}, |
| {"attach no positional host", []string{"domains", "attach", "-app", "tito"}, "usage: carlos domains attach"}, |
| - {"detach no app", []string{"domains", "detach", "tickets.example.com"}, "usage: carlos domains detach"}, |
| + {"detach no app", []string{"domains", "detach", "tickets.example.com"}, "no deployment target configured"}, |
| {"detach no positional host", []string{"domains", "detach", "-app", "tito"}, "usage: carlos domains detach"}, |
| - {"list no app", []string{"domains", "list"}, "usage: carlos domains list"}, |
| + {"list no app", []string{"domains", "list"}, "no deployment target configured"}, |
| } { |
| t.Run(tc.name, func(t *testing.T) { |
| out, err := run(tc.args...) |
diff --git a/instances_cli.go b/instances_cli.go| index d5054e56b..b7a1f63f1 100644 |
| --- a/instances_cli.go |
| +++ b/instances_cli.go |
| @@ -86,10 +86,7 @@ func runInstancesEnable(args []string) { |
| consoleFlag := fs.String("console", "", "console to enable through (signed-in mode only; default: the only logged-in one)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal(instancesEnableUsage) |
| - } |
| - if !release.ValidApp(*app) { |
| + if *app != "" && !release.ValidApp(*app) { |
| log.Fatalf("carlos instances enable: %q is not a valid app name", *app) |
| } |
| set := map[string]bool{} |
| @@ -125,7 +122,8 @@ func runInstancesEnable(args []string) { |
| } |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| - m, err := sess.instancesEnable(context.Background(), acct, *app, channelReq, seedReq) |
| + app2 := resolvedApp(sess, acct, *app, instancesEnableUsage) |
| + m, err := sess.instancesEnable(context.Background(), acct, app2, channelReq, seedReq) |
| if err != nil { |
| log.Fatalf("instances enable: %v", err) |
| } |
| @@ -139,6 +137,9 @@ func runInstancesEnable(args []string) { |
| // the "not a valid channel" refusal it has always been (the flag's |
| // default moved from "stable" to "" for the signed-in mode's |
| // preserve-vs-replace split; set[] is what keeps the two cases apart). |
| + if *app == "" { |
| + log.Fatal(instancesEnableUsage) |
| + } |
| if *domain == "" { |
| log.Fatal(instancesEnableUsage) |
| } |
| @@ -288,7 +289,7 @@ func runInstancesCreate(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| fs.Parse(args) |
| - if *app == "" || *host == "" { |
| + if *host == "" { |
| log.Fatal(instancesRecordsUsage) |
| } |
| set := map[string]bool{} |
| @@ -297,7 +298,9 @@ func runInstancesCreate(args []string) { |
| probe := parseProbeFlags(*healthPath, *healthStatus, set["health-path"], set["health-status"]) |
| sess := requireAPISession("create", *consoleFlag) |
| printUsing(stdout, sess) |
| - rec, err := sess.instanceCreate(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, instancesRecordsUsage) |
| + rec, err := sess.instanceCreate(context.Background(), acct, app2, |
| strings.TrimSpace(*host), strings.TrimSpace(*region), strings.TrimSpace(*channel), addrs, probe) |
| if err != nil { |
| log.Fatalf("instances create: %v", err) |
| @@ -365,11 +368,10 @@ func runInstancesList(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal(instancesRecordsUsage) |
| - } |
| sess := requireAPISession("list", *consoleFlag) |
| - recs, truncated, total, err := sess.instanceList(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, *match, *health) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, instancesRecordsUsage) |
| + recs, truncated, total, err := sess.instanceList(context.Background(), acct, app2, *match, *health) |
| if err != nil { |
| log.Fatalf("instances list: %v", err) |
| } |
| @@ -405,12 +407,14 @@ func runInstancesDelete(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| fs.Parse(args) |
| - if *app == "" || *host == "" { |
| + if *host == "" { |
| log.Fatal(instancesRecordsUsage) |
| } |
| sess := requireAPISession("delete", *consoleFlag) |
| printUsing(stdout, sess) |
| - if err := sess.instanceDelete(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, strings.TrimSpace(*host)); err != nil { |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, instancesRecordsUsage) |
| + if err := sess.instanceDelete(context.Background(), acct, app2, strings.TrimSpace(*host)); err != nil { |
| log.Fatalf("instances delete: %v", err) |
| } |
| fmt.Fprintf(stdout, "%s %s — its route retires on the next reconcile pass; parked data stays\n", |
| @@ -442,7 +446,7 @@ func runInstancesSetUpstreams(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| fs.Parse(args) |
| - if *app == "" || *host == "" { |
| + if *host == "" { |
| log.Fatal(instancesRecordsUsage) |
| } |
| set := map[string]bool{} |
| @@ -464,7 +468,9 @@ func runInstancesSetUpstreams(args []string) { |
| probe := parseProbeFlags(*healthPath, *healthStatus, set["health-path"], set["health-status"]) |
| sess := requireAPISession("set-upstreams", *consoleFlag) |
| printUsing(stdout, sess) |
| - rec, err := sess.instanceSetUpstreams(context.Background(), resolvedAccount(sess, acctRaw, acctIDRaw), *app, |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, instancesRecordsUsage) |
| + rec, err := sess.instanceSetUpstreams(context.Background(), acct, app2, |
| strings.TrimSpace(*host), addrs, probe) |
| if err != nil { |
| log.Fatalf("instances set-upstreams: %v", err) |
diff --git a/logs.go b/logs.go| index dd267d1db..5f338fc89 100644 |
| --- a/logs.go |
| +++ b/logs.go |
| @@ -71,9 +71,6 @@ func runLogs(args []string) { |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| fs.Usage = func() { fmt.Fprint(os.Stderr, logsUsage) } |
| fs.Parse(args) |
| - if *app == "" { |
| - log.Fatal(logsUsage) |
| - } |
| q := url.Values{} |
| for k, v := range map[string]string{ |
| @@ -93,8 +90,9 @@ func runLogs(args []string) { |
| sess := requireAPISession("logs", *consoleFlag) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, logsUsage) |
| ctx := context.Background() |
| - res, err := sess.logsGet(ctx, acct, *app, q) |
| + res, err := sess.logsGet(ctx, acct, app2, q) |
| if err != nil { |
| log.Fatalf("logs: %v", err) |
| } |
| @@ -127,7 +125,7 @@ func runLogs(args []string) { |
| if cursor != "" { |
| fq.Set("cursor", cursor) |
| } |
| - res, err := sess.logsGet(ctx, acct, *app, fq) |
| + res, err := sess.logsGet(ctx, acct, app2, fq) |
| if err != nil { |
| // A follow rides through a blip; the next poll answers or the |
| // member interrupts. Say it once per failure, quietly. |
diff --git a/restart.go b/restart.go| index c9d628b9c..213705f78 100644 |
| --- a/restart.go |
| +++ b/restart.go |
| @@ -27,19 +27,20 @@ func runRestart(args []string) { |
| consoleFlag := fs.String("console", "", "console origin to restart through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - if name == "" { |
| - log.Fatal("usage: carlos restart -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]") |
| - } |
| - // Client-side gate before the server's, the same one `carlos apps create` |
| - // applies: a name the console is certain to refuse should cost a typo |
| - // nothing, and the shared write budget is spent per attempt. |
| - if err := validAppName(name); err != nil { |
| - log.Fatalf("restart: %v", err) |
| + usage := "usage: carlos restart -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| + if name != "" { |
| + // Client-side gate before the server's, the same one `carlos apps create` |
| + // applies: a name the console is certain to refuse should cost a typo |
| + // nothing, and the shared write budget is spent per attempt. |
| + if err := validAppName(name); err != nil { |
| + log.Fatalf("restart: %v", err) |
| + } |
| } |
| sess := requireRestartAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + name = resolvedApp(sess, acct, name, usage) |
| res, err := sess.restart(context.Background(), acct, name) |
| if err != nil { |
| log.Fatalf("restart: %v", err) |
diff --git a/restart_test.go b/restart_test.go| index 8232da794..7469888ff 100644 |
| --- a/restart_test.go |
| +++ b/restart_test.go |
| @@ -122,12 +122,16 @@ func TestRestartVerbRefusals(t *testing.T) { |
| return string(out), err |
| } |
| + // -app is resolvable since the project-defaults spec (2026-08-13), so a |
| + // bare `carlos restart` with nothing configured stops on the REAL |
| + // blocker — no session — not on a usage line for a flag that is no |
| + // longer required. |
| out, err := run(nil, "restart") |
| if err == nil { |
| t.Fatalf("restart with no -app exited 0:\n%s", out) |
| } |
| - if !strings.Contains(out, "usage: carlos restart -app") { |
| - t.Fatalf("restart with no -app = %q, want the usage line", out) |
| + if !strings.Contains(out, "no deployment target configured") { |
| + t.Fatalf("bare restart = %q, want the no-target refusal", out) |
| } |
| // Direct bucket mode: refused in restart's OWN words, naming the login |
diff --git a/retention.go b/retention.go| index f4de48820..d31cdfc66 100644 |
| --- a/retention.go |
| +++ b/retention.go |
| @@ -30,11 +30,10 @@ func runReleasesRetention(args []string) { |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos releases retention -app <a> [-keep <n> [-dry-run] | -off] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| - if name == "" { |
| - log.Fatal(usage) |
| - } |
| - if err := validAppName(name); err != nil { |
| - log.Fatalf("releases retention: %v", err) |
| + if name != "" { |
| + if err := validAppName(name); err != nil { |
| + log.Fatalf("releases retention: %v", err) |
| + } |
| } |
| // Client-side gates before the server's: a request the console is |
| // certain to refuse should cost a typo nothing. |
| @@ -53,6 +52,7 @@ func runReleasesRetention(args []string) { |
| sess := requireRetentionAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + name = resolvedApp(sess, acct, name, usage) |
| ctx := context.Background() |
| switch { |
| case *off: |
diff --git a/retention_cli_test.go b/retention_cli_test.go| index c015e73e2..5ffe26ffa 100644 |
| --- a/retention_cli_test.go |
| +++ b/retention_cli_test.go |
| @@ -47,11 +47,14 @@ func TestReleasesRetentionRefusesOffPlusKeep(t *testing.T) { |
| } |
| func TestReleasesRetentionNeedsApp(t *testing.T) { |
| + // -app resolves since the project-defaults spec (2026-08-13): with |
| + // nothing configured the refusal names the missing SESSION, the real |
| + // blocker — not a usage line for a flag that is no longer required. |
| out, err := runRetentionCLI(t, "releases", "retention") |
| if err == nil { |
| t.Fatalf("no -app exited 0: %s", out) |
| } |
| - if !strings.Contains(out, "usage:") { |
| - t.Fatalf("want usage line, got: %s", out) |
| + if !strings.Contains(out, "no deployment target configured") { |
| + t.Fatalf("want the no-target refusal, got: %s", out) |
| } |
| } |
diff --git a/store.go b/store.go| index 05075cefe..bdca699e5 100644 |
| --- a/store.go |
| +++ b/store.go |
| @@ -132,19 +132,20 @@ func runStoreCreate(args []string) { |
| consoleFlag := fs.String("console", "", "console origin to declare through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - if name == "" { |
| - log.Fatal("usage: carlos store create -app <a> [-env-prefix <PREFIX>] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]") |
| - } |
| + usage := "usage: carlos store create -app <a> [-env-prefix <PREFIX>] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| // Client-side gate before the server's, the same one every write verb in |
| // this file applies: a name the console is certain to refuse should cost |
| // a typo nothing. |
| - if err := validAppName(name); err != nil { |
| - log.Fatalf("store create: %v", err) |
| + if name != "" { |
| + if err := validAppName(name); err != nil { |
| + log.Fatalf("store create: %v", err) |
| + } |
| } |
| sess := requireStoreAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + name = resolvedApp(sess, acct, name, usage) |
| res, err := sess.storeCreate(context.Background(), acct, name, strings.TrimSpace(*envPrefix)) |
| if err != nil { |
| log.Fatalf("store create: %v", err) |
| @@ -159,16 +160,17 @@ func runStoreStatus(args []string) { |
| consoleFlag := fs.String("console", "", "console origin to read through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - if name == "" { |
| - log.Fatal("usage: carlos store status -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]") |
| - } |
| - if err := validAppName(name); err != nil { |
| - log.Fatalf("store status: %v", err) |
| + usage := "usage: carlos store status -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| + if name != "" { |
| + if err := validAppName(name); err != nil { |
| + log.Fatalf("store status: %v", err) |
| + } |
| } |
| sess := requireStoreAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + name = resolvedApp(sess, acct, name, usage) |
| res, err := sess.storeGet(context.Background(), acct, name) |
| if err != nil { |
| log.Fatalf("store status: %v", err) |
| @@ -184,16 +186,17 @@ func runStoreGrant(args []string) { |
| consoleFlag := fs.String("console", "", "console origin to grant through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - if name == "" { |
| - log.Fatal("usage: carlos store grant -app <a> [-bucket <name>] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]") |
| - } |
| - if err := validAppName(name); err != nil { |
| - log.Fatalf("store grant: %v", err) |
| + usage := "usage: carlos store grant -app <a> [-bucket <name>] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| + if name != "" { |
| + if err := validAppName(name); err != nil { |
| + log.Fatalf("store grant: %v", err) |
| + } |
| } |
| sess := requireStoreAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + name = resolvedApp(sess, acct, name, usage) |
| res, err := sess.storeGrant(context.Background(), acct, name, strings.TrimSpace(*bucket)) |
| if err != nil { |
| log.Fatalf("store grant: %v", err) |
| @@ -209,16 +212,17 @@ func runStoreRotate(args []string) { |
| consoleFlag := fs.String("console", "", "console origin to rotate through (API mode only; default: the only logged-in one)") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - if name == "" { |
| - log.Fatal("usage: carlos store rotate -app <a> [-finish] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]") |
| - } |
| - if err := validAppName(name); err != nil { |
| - log.Fatalf("store rotate: %v", err) |
| + usage := "usage: carlos store rotate -app <a> [-finish] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| + if name != "" { |
| + if err := validAppName(name); err != nil { |
| + log.Fatalf("store rotate: %v", err) |
| + } |
| } |
| sess := requireStoreAPISession(*consoleFlag) |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + name = resolvedApp(sess, acct, name, usage) |
| res, err := sess.storeRotate(context.Background(), acct, name, *finish) |
| if err != nil { |
| log.Fatalf("store rotate: %v", err) |
diff --git a/store_test.go b/store_test.go| index db046ab76..cf1f6af97 100644 |
| --- a/store_test.go |
| +++ b/store_test.go |
| @@ -526,22 +526,16 @@ func TestStoreVerbDispatchAndRefusals(t *testing.T) { |
| } |
| } |
| - // Missing -app, one representative per sub-verb (each builds its own |
| - // usage line — the shape restart.go's own missing-app test pins). |
| - for _, tc := range []struct { |
| - sub, want string |
| - }{ |
| - {"create", "usage: carlos store create -app"}, |
| - {"status", "usage: carlos store status -app"}, |
| - {"grant", "usage: carlos store grant -app"}, |
| - {"rotate", "usage: carlos store rotate -app"}, |
| - } { |
| - out, err := run(nil, "store", tc.sub) |
| + // -app is resolvable since the project-defaults spec (2026-08-13), so a |
| + // missing -app with nothing configured stops on the REAL blocker — no |
| + // session — not on a usage line for a flag that is no longer required. |
| + for _, sub := range []string{"create", "status", "grant", "rotate"} { |
| + out, err := run(nil, "store", sub) |
| if err == nil { |
| - t.Fatalf("carlos store %s with no -app exited 0:\n%s", tc.sub, out) |
| + t.Fatalf("carlos store %s with no -app exited 0:\n%s", sub, out) |
| } |
| - if !strings.Contains(out, tc.want) { |
| - t.Fatalf("carlos store %s with no -app = %q, want %q", tc.sub, out, tc.want) |
| + if !strings.Contains(out, "no deployment target configured") { |
| + t.Fatalf("carlos store %s with no -app = %q, want the no-target refusal", sub, out) |
| } |
| } |