@@ -53,7 +53,7 @@ const MATRIX = {
5353 shared : [ "status" , "create" , "fund" , "balance" , "export" ] ,
5454 specific : [ "checkout" , "history" ] ,
5555 } ,
56- tier : { shared : [ "status" , "set" ] , specific : [ ] } ,
56+ tier : { shared : [ ] , specific : [ "status" , "set" ] } ,
5757 projects : {
5858 shared : [
5959 "quote" , "use" , "list" , "info" , "keys" , "rest" ,
@@ -63,48 +63,49 @@ const MATRIX = {
6363 } ,
6464 deploy : { shared : [ ] , specific : [ ] } ,
6565 functions : {
66- shared : [ "list" , "delete" ] ,
67- specific : [ "deploy" , "invoke" , "logs" , "update" ] ,
66+ shared : [ ] ,
67+ specific : [ "deploy" , "invoke" , "logs" , "update" , "list" , "delete" ] ,
6868 } ,
69- secrets : { shared : [ "list" , "delete" ] , specific : [ "set" ] } ,
69+ secrets : { shared : [ ] , specific : [ "set" , "list" , "delete "] } ,
7070 blob : {
7171 shared : [ ] ,
7272 specific : [ "put" , "get" , "ls" , "rm" , "sign" ] ,
7373 } ,
7474 sites : { shared : [ "status" ] , specific : [ "deploy" , "deploy-dir" ] } ,
75- subdomains : { shared : [ "delete ", "list" ] , specific : [ "claim "] } ,
76- domains : { shared : [ "add" , "list" , "status" , "delete" ] , specific : [ ] } ,
75+ subdomains : { shared : [ ] , specific : [ "claim ", "list" , "delete "] } ,
76+ domains : { shared : [ ] , specific : [ "add" , "list" , "status" , "delete" ] } ,
7777 apps : {
78- shared : [ "versions" , "inspect" , "delete" ] ,
79- specific : [ "browse" , "fork" , "publish" , "update" ] ,
78+ shared : [ ] ,
79+ specific : [ "browse" , "fork" , "publish" , "update" , "versions" , "inspect" , "delete" ] ,
8080 } ,
81- ai : { shared : [ "moderate" , "usage" ] , specific : [ "translate" ] } ,
82- image : { shared : [ "generate" ] , specific : [ ] } ,
81+ ai : { shared : [ ] , specific : [ "translate" , "moderate" , "usage "] } ,
82+ image : { shared : [ ] , specific : [ "generate" ] } ,
8383 email : {
84- shared : [ "create" , "get" ] ,
85- specific : [ "info" , "status" , "send" , "list" , "get-raw" , "reply" , "delete" ] ,
84+ shared : [ ] ,
85+ specific : [ "info" , "status" , "send" , "list" , "get-raw" , "reply" , "delete" , "create" , "get" ] ,
8686 } ,
87- message : { shared : [ "send" ] , specific : [ ] } ,
87+ message : { shared : [ ] , specific : [ "send" ] } ,
8888 auth : {
8989 shared : [ ] ,
9090 specific : [ "magic-link" , "verify" , "set-password" , "settings" , "providers" ] ,
9191 } ,
9292 "sender-domain" : {
93- shared : [ "register" , "status" , "remove" , "inbound-enable" , "inbound-disable" ] ,
94- specific : [ ] ,
93+ shared : [ ] ,
94+ specific : [ "register" , "status" , "remove" , "inbound-enable" , "inbound-disable" ] ,
9595 } ,
9696 billing : {
97- shared : [ "create-email" , "link-wallet" , "balance" ] ,
98- specific : [ "tier-checkout" , "buy-email-pack" , "auto-recharge" , "history" ] ,
97+ shared : [ ] ,
98+ specific : [ "tier-checkout" , "buy-email-pack" , "auto-recharge" , "history" , "create-email" , "link-wallet" , "balance" ] ,
9999 } ,
100100 contracts : {
101- shared : [ "get-wallet" , "list-wallets" , "status" ] ,
101+ shared : [ ] ,
102102 specific : [
103103 "provision-wallet" , "set-recovery" , "set-alert" , "call" , "read" , "drain" , "delete" ,
104+ "get-wallet" , "list-wallets" , "status" ,
104105 ] ,
105106 } ,
106- agent : { shared : [ "contact" ] , specific : [ ] } ,
107- service : { shared : [ "status" , "health" ] , specific : [ ] } ,
107+ agent : { shared : [ ] , specific : [ "contact" ] } ,
108+ service : { shared : [ ] , specific : [ "status" , "health" ] } ,
108109} ;
109110
110111// `run402 email webhooks <action>` delegates to lib/webhooks.mjs.
@@ -272,4 +273,81 @@ describe("CLI --help contract", () => {
272273 } ) ;
273274 }
274275 } ) ;
276+
277+ // GH-198 — `run402 deploy --help` was showing the v1 bundle manifest format
278+ // (top-level `migrations` string, `secrets` array, `functions` array, `files`
279+ // array) which the v2 gateway rejects. The example must match the v2
280+ // ReleaseSpec shape documented in cli/llms-cli.txt: object trees rooted at
281+ // `database`, `site`, `functions.replace`, `secrets.set`, `subdomains`.
282+ describe ( "run402 deploy --help shows v2 manifest format (GH-198)" , ( ) => {
283+ it ( "deploy --help example uses v2 keys (database/site/replace), not v1 arrays" , async ( ) => {
284+ const result = await runCli ( [ "deploy" , "--help" ] ) ;
285+ assertHelp ( result , "run402 deploy --help" ) ;
286+
287+ const out = result . stdout ;
288+
289+ // v2 keys must be present in the example block.
290+ assert . match ( out , / " d a t a b a s e " : / ,
291+ `deploy --help: example must contain a top-level "database": key (v2 ReleaseSpec)\nstdout:\n${ out } ` ) ;
292+ assert . match ( out , / " s i t e " : / ,
293+ `deploy --help: example must contain a top-level "site": key (v2 ReleaseSpec)\nstdout:\n${ out } ` ) ;
294+ assert . match ( out , / " r e p l a c e " : / ,
295+ `deploy --help: example must contain a "replace": key (v2 site/functions shape)\nstdout:\n${ out } ` ) ;
296+ assert . match ( out , / " s e t " : / ,
297+ `deploy --help: example must contain a "set": key (v2 secrets/subdomains shape)\nstdout:\n${ out } ` ) ;
298+ assert . match ( out , / " s u b d o m a i n s " : / ,
299+ `deploy --help: example must contain a "subdomains": key (v2 ReleaseSpec)\nstdout:\n${ out } ` ) ;
300+
301+ // v1 shapes that are NOT accepted by the v2 gateway must be gone.
302+ // - top-level `"migrations":` as a string (v1) — v2 nests under database.migrations as an array of {id, sql}
303+ assert . doesNotMatch ( out , / ^ \s * " m i g r a t i o n s " : \s * " / m,
304+ `deploy --help: example must not have v1 top-level "migrations": "<string>" (use database.migrations: [{id, sql}])\nstdout:\n${ out } ` ) ;
305+ // - top-level `"files":` as an array (v1) — v2 uses site.replace as an object map
306+ assert . doesNotMatch ( out , / ^ \s * " f i l e s " : \s * \[ / m,
307+ `deploy --help: example must not have v1 top-level "files": [...] array (use site.replace: { "<path>": {...} })\nstdout:\n${ out } ` ) ;
308+ // - top-level `"secrets":` as an array (v1) — v2 uses secrets.set as an object map
309+ assert . doesNotMatch ( out , / ^ \s * " s e c r e t s " : \s * \[ / m,
310+ `deploy --help: example must not have v1 top-level "secrets": [...] array (use secrets.set: { "<KEY>": {...} })\nstdout:\n${ out } ` ) ;
311+ // - top-level `"functions":` as an array (v1) — v2 uses functions.replace as an object map
312+ assert . doesNotMatch ( out , / ^ \s * " f u n c t i o n s " : \s * \[ / m,
313+ `deploy --help: example must not have v1 top-level "functions": [...] array (use functions.replace: { "<name>": {...} })\nstdout:\n${ out } ` ) ;
314+ // - top-level `"subdomain":` (singular, v1) — v2 uses `subdomains.set: ["..."]`
315+ assert . doesNotMatch ( out , / ^ \s * " s u b d o m a i n " : / m,
316+ `deploy --help: example must not have v1 top-level "subdomain" (singular); use "subdomains": { "set": [...] }\nstdout:\n${ out } ` ) ;
317+ } ) ;
318+ } ) ;
319+
320+ // Regression test for GH-188: confirm the previously-broken subcommands now
321+ // print their own per-subcommand help instead of falling back to the parent
322+ // namespace's help page. The MATRIX above also covers these (each one moved
323+ // from `shared` to `specific`), but this explicit suite spot-checks a
324+ // representative sample across namespaces and asserts both that the
325+ // subcommand-specific title is present AND that the parent's general
326+ // "Manage ..." headline is NOT.
327+ describe ( "GH-188 regression — SUB_HELP entries for previously-broken subs" , ( ) => {
328+ const cases = [
329+ // [command sequence, parent-help heading that should NOT appear]
330+ [ [ "secrets" , "list" ] , "run402 secrets — Manage project secrets" ] ,
331+ [ [ "functions" , "list" ] , "run402 functions — Manage serverless functions" ] ,
332+ [ [ "domains" , "list" ] , "run402 domains — Manage custom domains" ] ,
333+ [ [ "ai" , "moderate" ] , "run402 ai — AI translation and moderation tools" ] ,
334+ [ [ "tier" , "status" ] , "run402 tier — Manage your Run402 tier subscription" ] ,
335+ [ [ "service" , "status" ] , "run402 service — Run402 service health and availability" ] ,
336+ [ [ "sender-domain" , "register" ] , "run402 sender-domain — Manage custom email sender domain" ] ,
337+ [ [ "contracts" , "get-wallet" ] , "run402 contracts — KMS-backed Ethereum wallets" ] ,
338+ ] ;
339+ for ( const [ argv , parentHeading ] of cases ) {
340+ it ( `run402 ${ argv . join ( " " ) } --help shows per-subcommand title` , async ( ) => {
341+ const result = await runCli ( [ ...argv , "--help" ] ) ;
342+ const expectedHeading = `run402 ${ argv . join ( " " ) } ` ;
343+ assertHelp ( result , `run402 ${ argv . join ( " " ) } --help` , {
344+ expectHeadingStartsWith : expectedHeading ,
345+ } ) ;
346+ // Belt-and-suspenders: parent-only headline must NOT be the first line.
347+ const firstLine = result . stdout . trimStart ( ) . split ( / \r ? \n / , 1 ) [ 0 ] ;
348+ assert . ok ( ! firstLine . startsWith ( parentHeading . split ( " — " ) [ 0 ] + " —" ) ,
349+ `run402 ${ argv . join ( " " ) } --help fell through to parent help: first line was "${ firstLine } "` ) ;
350+ } ) ;
351+ }
352+ } ) ;
275353} ) ;
0 commit comments