carlos / platform Public

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>
Paul Campbell b5b9488ac9e66e0f79c8587dc25dcb01f48e4955 parent 1f5ddbb
13 files changed, +308 −191
  • appconfig_cli.go +88 −47
  • cli.go +115 −57
  • deploy.go +6 −4
  • domains_cli.go +11 −9
  • domains_cli_test.go +7 −3
  • instances_cli.go +21 −15
  • logs.go +3 −5
  • restart.go +9 −8
  • restart_test.go +6 −2
  • retention.go +5 −5
  • retention_cli_test.go +5 −2
  • store.go +24 −20
  • store_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)
}
}