@supacloud/cli 0.96.2 → 0.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24387,6 +24387,302 @@ test("${name} controller preserves an asynchronous read failure", async () => {
24387
24387
  };
24388
24388
  }
24389
24389
 
24390
+ // src/shared/tools/app-feature.ts
24391
+ function featureScaffold(name, className, governance) {
24392
+ const commandName = `${name}.approve`;
24393
+ const { permission, rpc } = governance;
24394
+ return {
24395
+ [`${name}.contracts.ts`]: `import { t } from "elysia";
24396
+
24397
+ export const ${className}Params = t.Object({
24398
+ id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
24399
+ });
24400
+ export const ${className}ListQuery = t.Object({
24401
+ state: t.Optional(t.String({ minLength: 1, maxLength: 64 })),
24402
+ });
24403
+ export const ${className}ApproveBody = t.Object({
24404
+ expectedVersion: t.Integer({ minimum: 1 }),
24405
+ reason: t.Optional(t.String({ maxLength: 2000 })),
24406
+ });
24407
+ export const ${className}Row = t.Object({
24408
+ id: t.String(),
24409
+ state: t.String(),
24410
+ version: t.Integer({ minimum: 1 }),
24411
+ });
24412
+ export const ${className}ListResponse = t.Array(${className}Row);
24413
+ `,
24414
+ [`${name}.model.ts`]: `import { InjectionToken } from "@supacloud/app/core";
24415
+ import type {
24416
+ ${className}ApproveBody,
24417
+ ${className}ListQuery,
24418
+ ${className}Row,
24419
+ } from "./${name}.contracts";
24420
+ import type { StaticDecode } from "typebox";
24421
+
24422
+ export type ${className}ListInput = StaticDecode<typeof ${className}ListQuery>;
24423
+ export type ${className}ApproveInput = StaticDecode<typeof ${className}ApproveBody>;
24424
+ export type ${className}Result = StaticDecode<typeof ${className}Row>;
24425
+
24426
+ export interface ${className}ReadPort {
24427
+ list(input: ${className}ListInput): Promise<${className}Result[]>;
24428
+ detail(id: string): Promise<${className}Result>;
24429
+ }
24430
+
24431
+ export interface ${className}WritePort {
24432
+ approve(input: {
24433
+ id: string;
24434
+ expectedVersion: number;
24435
+ reason?: string;
24436
+ }): Promise<${className}Result>;
24437
+ }
24438
+
24439
+ export const ${className}Reader = new InjectionToken<${className}ReadPort>(
24440
+ "${name}.reader", { scope: "request" },
24441
+ );
24442
+ export const ${className}Writer = new InjectionToken<${className}WritePort>(
24443
+ "${name}.writer", { scope: "request" },
24444
+ );
24445
+ `,
24446
+ [`${name}.service.ts`]: `import { Inject, Injectable, Optional } from "@supacloud/app/core";
24447
+ import {
24448
+ ${className}Reader,
24449
+ ${className}Writer,
24450
+ type ${className}ApproveInput,
24451
+ type ${className}ReadPort,
24452
+ type ${className}Result,
24453
+ type ${className}WritePort,
24454
+ } from "./${name}.model";
24455
+
24456
+ @Injectable({ scope: "request" })
24457
+ export class ${className}Service {
24458
+ constructor(
24459
+ @Inject(${className}Reader) @Optional() private readonly reader?: ${className}ReadPort,
24460
+ @Inject(${className}Writer) @Optional() private readonly writer?: ${className}WritePort,
24461
+ ) {}
24462
+
24463
+ async list(input: Parameters<${className}ReadPort["list"]>[0]): Promise<${className}Result[]> {
24464
+ if (!this.reader) throw new Error("Bind ${className}Reader with verified identity and tenant scope");
24465
+ return this.reader.list(input);
24466
+ }
24467
+
24468
+ async detail(id: string): Promise<${className}Result> {
24469
+ if (!this.reader) throw new Error("Bind ${className}Reader with verified identity and tenant scope");
24470
+ return this.reader.detail(id);
24471
+ }
24472
+
24473
+ async approve(id: string, input: ${className}ApproveInput): Promise<${className}Result> {
24474
+ if (!this.writer) throw new Error("Bind ${className}Writer with durable command governance");
24475
+ return this.writer.approve({ ...input, id });
24476
+ }
24477
+ }
24478
+ `,
24479
+ [`${name}.feature.ts`]: `import { Effect } from "effect";
24480
+ import {
24481
+ Body, Command, Controller, defineFeatureSlice, defineFeatureSpec, Get, Inject,
24482
+ Injectable, Job, Param, Post, Query, type FeatureState,
24483
+ } from "@supacloud/app/core";
24484
+ import {
24485
+ ${className}ApproveBody,
24486
+ ${className}ListQuery,
24487
+ ${className}ListResponse,
24488
+ ${className}Params,
24489
+ ${className}Row,
24490
+ } from "./${name}.contracts";
24491
+ import { ${className}Service } from "./${name}.service";
24492
+ import type { ${className}ApproveInput, ${className}ListInput, ${className}Result } from "./${name}.model";
24493
+
24494
+ export const featureSpec = defineFeatureSpec({
24495
+ name: ${JSON.stringify(name)},
24496
+ states: ["draft", "approved"],
24497
+ transitions: {
24498
+ approve: {
24499
+ from: "draft", to: "approved", command: "Approve${className}Command",
24500
+ permission: ${JSON.stringify(permission)}, transaction: "required",
24501
+ idempotency: "required", audit: ${JSON.stringify(`${name}.approved`)},
24502
+ route: "POST /${name}/:id/approve",
24503
+ },
24504
+ },
24505
+ });
24506
+ export type ${className}State = FeatureState<typeof featureSpec>;
24507
+
24508
+ @Injectable({ scope: "request" })
24509
+ @Query({ name: "${name}.list" })
24510
+ export class List${className}Query {
24511
+ constructor(@Inject(${className}Service) private readonly service: ${className}Service) {}
24512
+ execute(input: ${className}ListInput): Promise<${className}Result[]> {
24513
+ return this.service.list(input);
24514
+ }
24515
+ }
24516
+
24517
+ @Injectable({ scope: "request" })
24518
+ @Command({
24519
+ name: ${JSON.stringify(commandName)},
24520
+ permission: ${JSON.stringify(permission)},
24521
+ transaction: "required",
24522
+ audit: ${JSON.stringify(`${name}.approved`)},
24523
+ idempotency: "required",
24524
+ rpc: ${JSON.stringify(rpc)},
24525
+ })
24526
+ export class Approve${className}Command {
24527
+ constructor(@Inject(${className}Service) private readonly service: ${className}Service) {}
24528
+ execute(id: string, input: ${className}ApproveInput): Promise<${className}Result> {
24529
+ return this.service.approve(id, input);
24530
+ }
24531
+ }
24532
+
24533
+ @Job({
24534
+ name: ${JSON.stringify(`${name}.refresh`)},
24535
+ mode: "task",
24536
+ idempotency: "required",
24537
+ })
24538
+ export class Refresh${className}Job {
24539
+ run(_input: { id: string; version: number }): Promise<void> {
24540
+ throw new Error("Bind the existing Workflow/Queue adapter before dispatching ${name}.refresh");
24541
+ }
24542
+ }
24543
+
24544
+ @Controller("/${name}")
24545
+ export class ${className}Controller {
24546
+ constructor(
24547
+ @Inject(List${className}Query) private readonly listQuery: List${className}Query,
24548
+ @Inject(Approve${className}Command) private readonly approveCommand: Approve${className}Command,
24549
+ @Inject(${className}Service) private readonly service: ${className}Service,
24550
+ ) {}
24551
+
24552
+ @Get("/", {
24553
+ query: ${className}ListQuery,
24554
+ responses: { 200: ${className}ListResponse },
24555
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
24556
+ })
24557
+ list(@Query("state") state?: string): Effect.Effect<${className}Result[], never, never> {
24558
+ return Effect.promise(() => this.listQuery.execute(state === undefined ? {} : { state }));
24559
+ }
24560
+
24561
+ @Get("/:id", {
24562
+ params: ${className}Params,
24563
+ responses: { 200: ${className}Row },
24564
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
24565
+ })
24566
+ detail(@Param("id") id: string): Effect.Effect<${className}Result, never, never> {
24567
+ return Effect.promise(() => this.service.detail(id));
24568
+ }
24569
+
24570
+ @Post("/:id/approve", {
24571
+ params: ${className}Params,
24572
+ body: ${className}ApproveBody,
24573
+ responses: { 200: ${className}Row },
24574
+ command: Approve${className}Command,
24575
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
24576
+ })
24577
+ approve(
24578
+ @Param("id") id: string,
24579
+ @Body() body: ${className}ApproveInput,
24580
+ ): Effect.Effect<${className}Result, never, never> {
24581
+ return Effect.promise(() => this.approveCommand.execute(id, body));
24582
+ }
24583
+ }
24584
+
24585
+ export const ${className}Feature = defineFeatureSlice({
24586
+ name: ${JSON.stringify(name)},
24587
+ tags: ["type:feature"],
24588
+ spec: featureSpec,
24589
+ providers: [${className}Service, List${className}Query, Approve${className}Command],
24590
+ controllers: [${className}Controller],
24591
+ jobs: [Refresh${className}Job],
24592
+ });
24593
+ `,
24594
+ [`${name}.feature.test.ts`]: `import { expect, test } from "bun:test";
24595
+ import { ${className}Service } from "./${name}.service";
24596
+
24597
+ test("${name} fails closed until verified read and durable write ports are bound", async () => {
24598
+ const service = new ${className}Service();
24599
+ await expect(service.list({})).rejects.toThrow("Bind ${className}Reader");
24600
+ await expect(service.detail("example")).rejects.toThrow("Bind ${className}Reader");
24601
+ await expect(service.approve("example", { expectedVersion: 1 })).rejects.toThrow("Bind ${className}Writer");
24602
+ });
24603
+
24604
+ test("${name} preserves denied reads and rejected writes without fallback or retry", async () => {
24605
+ const denied = new Error("denied");
24606
+ let attempts = 0;
24607
+ const service = new ${className}Service({
24608
+ list: async () => { throw denied; },
24609
+ detail: async () => { throw denied; },
24610
+ }, {
24611
+ approve: async () => { attempts++; throw denied; },
24612
+ });
24613
+ await expect(service.list({})).rejects.toBe(denied);
24614
+ await expect(service.detail("example")).rejects.toBe(denied);
24615
+ await expect(service.approve("example", { expectedVersion: 1 })).rejects.toBe(denied);
24616
+ expect(attempts).toBe(1);
24617
+ });
24618
+
24619
+ test("${name} forwards the authoritative version and result without inventing persistence", async () => {
24620
+ const row = { id: "example", state: "approved", version: 2 };
24621
+ const service = new ${className}Service(undefined, {
24622
+ async approve(input) {
24623
+ expect(input).toEqual({ id: "example", expectedVersion: 1, reason: "reviewed" });
24624
+ return row;
24625
+ },
24626
+ });
24627
+ expect(await service.approve("example", { expectedVersion: 1, reason: "reviewed" })).toBe(row);
24628
+ });
24629
+ `,
24630
+ [`${name}.deployment.md`]: `# ${className} feature delivery checklist
24631
+
24632
+ - [ ] Bind ${className}Reader to a tenant-scoped, authenticated database read path.
24633
+ - [ ] Bind ${className}Writer to a transactional RPC or persistent Command adapter.
24634
+ - [ ] Register the \`${rpc}\` compiler and runtime RPC capability.
24635
+ - [ ] Verify RLS, permission denial, stale version, identical replay and conflicting replay.
24636
+ - [ ] Verify the refresh job uses the existing Workflow/Queue delivery path.
24637
+ - [ ] Run the generated client contract and authenticated E2E test.
24638
+ - [ ] Run \`supacloud app check --format json\`, package tests and the selected deployment receipt.
24639
+
24640
+ The scaffold does not create migrations, infer permissions, or provide an in-memory
24641
+ production fallback. Database migrations remain the database source of truth.
24642
+ The HTTP schemas describe the boundary, not a second database model. Bind their
24643
+ fields to the existing database-source contract checker before delivery.
24644
+
24645
+ The named RPC adapter must invoke the continuation in the same database transaction
24646
+ used by the request-scoped Writer, or own the complete command instead; never execute
24647
+ both paths. Lock/check the version and state, reauthorize replays, and commit the
24648
+ business write, receipt, audit and outbox together. Do not put a network side effect
24649
+ inside this transaction. The refresh Job is an explicit unimplemented integration
24650
+ point, not an automatically scheduled workflow.
24651
+
24652
+ Compile the routes with the existing client generator and use @supacloud/query for
24653
+ identity-scoped caching/invalidation. There is deliberately no second hand-maintained
24654
+ HTTP client. A committed approval followed by a failed refresh is still committed;
24655
+ recover its receipt instead of resending. Use the host's standard error translation
24656
+ and requestId propagation; the scaffold does not claim to implement those adapters.
24657
+
24658
+ ## Required acceptance (not executed by generation)
24659
+
24660
+ \`\`\`gherkin
24661
+ Scenario: Authorization and tenant isolation
24662
+ Given two tenants and a revoked reviewer
24663
+ When list, detail, approval or receipt replay is requested
24664
+ Then unauthorized records and prior success receipts are not disclosed
24665
+
24666
+ Scenario: Atomic approval
24667
+ Given a draft and its authoritative version
24668
+ When an authorized reviewer approves and repeats the same request
24669
+ Then one transition, audit event and asynchronous intent commit
24670
+ And changed input or a stale version is rejected
24671
+
24672
+ Scenario: Unknown outcome
24673
+ Given a committed approval whose response was lost
24674
+ When the client recovers the original receipt
24675
+ Then it does not create a new approval or automatic compensation
24676
+
24677
+ Scenario: Delivery
24678
+ Given verified migrations, RLS and generated client contracts
24679
+ When the selected environment is deployed
24680
+ Then its receipt and authenticated list/detail/approval flow are read back
24681
+ \`\`\`
24682
+ `
24683
+ };
24684
+ }
24685
+
24390
24686
  // src/shared/tools/app-local-dev.ts
24391
24687
  import { spawn } from "node:child_process";
24392
24688
  import { readFile as readFile2 } from "node:fs/promises";
@@ -24878,6 +25174,12 @@ function requireIdentifier(value, flag) {
24878
25174
  }
24879
25175
  return trimmed;
24880
25176
  }
25177
+ function requireFeatureCapability(value, flag) {
25178
+ if (typeof value !== "string" || !/^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/.test(value)) {
25179
+ throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", `feature requires an explicit valid --${flag}; capabilities are not inferred`);
25180
+ }
25181
+ return value;
25182
+ }
24881
25183
  function moduleScaffold(name) {
24882
25184
  return `import { Module } from "@supacloud/app";
24883
25185
 
@@ -24896,6 +25198,9 @@ function commandScaffold(moduleName, name) {
24896
25198
  @Command({
24897
25199
  name: ${JSON.stringify(`${moduleName}.${camelName(name)}`)},
24898
25200
  permission: ${JSON.stringify(`${moduleName}.${camelName(name)}`)},
25201
+ transaction: "required",
25202
+ audit: ${JSON.stringify(`${moduleName}.${camelName(name)}.completed`)},
25203
+ idempotency: "required",
24899
25204
  })
24900
25205
  export class ${pascalName(name)}Command {
24901
25206
  execute(): void {
@@ -24950,8 +25255,8 @@ export const ${prefix}Response = t.Object({});
24950
25255
  }
24951
25256
  async function generateScaffold(args) {
24952
25257
  const kind = args.kind;
24953
- if (!kind || !["module", "command", "query", "controller", "job", "contract", "resource"].includes(kind)) {
24954
- throw new ScaffoldError("SCAFFOLD_KIND_INVALID", "app generate requires --kind (module|command|query|controller|job|contract|resource)");
25258
+ if (!kind || !["module", "command", "query", "controller", "job", "contract", "resource", "feature"].includes(kind)) {
25259
+ throw new ScaffoldError("SCAFFOLD_KIND_INVALID", "app generate requires --kind (module|command|query|controller|job|contract|resource|feature)");
24955
25260
  }
24956
25261
  for (const flag of ["dry_run", "force"]) {
24957
25262
  if (args[flag] !== undefined && typeof args[flag] !== "boolean") {
@@ -24962,24 +25267,31 @@ async function generateScaffold(args) {
24962
25267
  const dir = args.dir ?? "src/features";
24963
25268
  if (dir !== ".")
24964
25269
  scaffoldPath(root, dir);
24965
- if (args.register_in !== undefined && kind !== "resource" && kind !== "module") {
24966
- throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", "--register-in is only supported for a new module or resource");
25270
+ if (args.register_in !== undefined && kind !== "resource" && kind !== "module" && kind !== "feature") {
25271
+ throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", "--register-in is only supported for a new module, resource or feature");
25272
+ }
25273
+ if ((kind === "resource" || kind === "feature") && (args.force || args.module !== undefined)) {
25274
+ throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", `${kind} creates its own module; use --name and optionally --register-in, never --force or --module`);
24967
25275
  }
24968
- if (kind === "resource" && (args.force || args.module !== undefined)) {
24969
- throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", "resource creates its own module; use --name and optionally --register-in, never --force or --module");
25276
+ if (kind !== "feature" && (args.permission !== undefined || args.rpc !== undefined)) {
25277
+ throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", "--permission and --rpc are only supported for feature generation");
24970
25278
  }
25279
+ const featureGovernance = kind === "feature" ? {
25280
+ permission: requireFeatureCapability(args.permission, "permission"),
25281
+ rpc: requireFeatureCapability(args.rpc, "rpc")
25282
+ } : undefined;
24971
25283
  const writes = [];
24972
25284
  let modulePath;
24973
25285
  let moduleSymbol;
24974
25286
  let contractName;
24975
- if (kind === "module" || kind === "resource") {
25287
+ if (kind === "module" || kind === "resource" || kind === "feature") {
24976
25288
  const name = requireIdentifier(args.name, "name");
24977
- modulePath = join13(dir, name, `${name}.module.ts`).replaceAll("\\", "/");
24978
- moduleSymbol = `${pascalName(name)}Module`;
25289
+ modulePath = join13(dir, name, kind === "feature" ? `${name}.feature.ts` : `${name}.module.ts`).replaceAll("\\", "/");
25290
+ moduleSymbol = kind === "feature" ? `${pascalName(name)}Feature` : `${pascalName(name)}Module`;
24979
25291
  if (kind === "module")
24980
25292
  writes.push({ path: modulePath, content: moduleScaffold(name), overwrite: args.force === true });
24981
25293
  else
24982
- for (const [file, content] of Object.entries(resourceScaffold(name, pascalName(name)))) {
25294
+ for (const [file, content] of Object.entries(kind === "feature" ? featureScaffold(name, pascalName(name), featureGovernance) : resourceScaffold(name, pascalName(name)))) {
24983
25295
  writes.push({ path: join13(dir, name, file).replaceAll("\\", "/"), content });
24984
25296
  }
24985
25297
  } else {
@@ -25041,6 +25353,8 @@ async function generateScaffold(args) {
25041
25353
  lines.push("No files written. Omit --dry-run to apply this generation.");
25042
25354
  if (kind === "resource")
25043
25355
  lines.push("Service and controller are registered in the generated module. No root module is guessed.", "Use --register-in <module.ts> to add an explicit parent import. Implement the read port before exposing the resource.", "Run app compile, app check --format json and your tests. No writes, permissions or persistence are inferred.");
25356
+ if (kind === "feature")
25357
+ lines.push(`Approval feature scaffold ${args.dry_run ? "planned" : "created"}; no database, authorization or job adapter is fabricated.`, "Bind the explicit RPC in compiler capabilities and runtime governance. Review the generated delivery checklist.", "Generate the client through app compile; no handwritten transport or database schema is created.");
25044
25358
  if (contractName) {
25045
25359
  const prefix = pascalName(contractName);
25046
25360
  lines.push("", "Next, bind the contract to a route so the compiler can check route/schema drift:", ` import { ${prefix}Body, ${prefix}Response } from "./contracts/${contractName}.contract";`, ` @Post("/", { body: ${prefix}Body, responses: { 200: ${prefix}Response } })`, "Fill in the schema fields and reuse them in command decoders; never widen them to unknown.");
@@ -26330,15 +26644,17 @@ function registerAppTools(server, options = {}) {
26330
26644
  "promote-status",
26331
26645
  "promote-reconcile"
26332
26646
  ]), "App action; promote obtains and verifies its plan before one mutation request"),
26333
- kind: optional(stringEnum(["module", "command", "query", "controller", "job", "contract", "resource"]), "[generate] Scaffold kind"),
26647
+ kind: optional(stringEnum(["module", "command", "query", "controller", "job", "contract", "resource", "feature"]), "[generate] Scaffold kind"),
26334
26648
  template: optional(stringEnum(["minimal", "http", "command", "edge"]), "[init] Minimal application by default; explicit http/command/edge recipes"),
26335
26649
  name: optional(String2(), "[init/generate] Project or object name"),
26336
26650
  module: optional(String2(), "[generate] Target feature module (required for command/query/controller)"),
26651
+ permission: optional(String2(), "[generate feature] Explicit approval permission; not inferred from name"),
26652
+ rpc: optional(String2(), "[generate feature] Explicit durable RPC capability; must be registered by the host"),
26337
26653
  dir: optional(String2(), "[generate] Feature root directory (default: src/features)"),
26338
- force: optional(Boolean2(), "[generate] Overwrite a single regular file; resource/controller never overwrite"),
26654
+ force: optional(Boolean2(), "[generate] Overwrite a single regular file; resource/feature/controller never overwrite"),
26339
26655
  dry_run: optional(Boolean2(), "[generate] Validate and preview the complete change set without writing"),
26340
26656
  "dry-run": optional(Boolean2(), "[generate] CLI alias of dry_run"),
26341
- register_in: optional(String2(), "[generate] Existing parent module file for module/resource; uses the compiler AST editor"),
26657
+ register_in: optional(String2(), "[generate] Existing parent module file for module/resource/feature; uses the compiler AST editor"),
26342
26658
  "register-in": optional(String2(), "[generate] CLI alias of register_in"),
26343
26659
  root: optional(String2(), "[dev/plan/build/release/compile/check] Project directory containing supacloud.config.ts (default: current directory)"),
26344
26660
  include: optional(String2(), "[dev/compile/check] Comma-separated glob patterns for source files"),
@@ -26360,14 +26676,16 @@ function registerAppTools(server, options = {}) {
26360
26676
  var APP_ALIAS_ACTIONS = ["generate", "compile", "check", "graph", "explain", "context", "doctor", "fix"];
26361
26677
  function registerAppAliases(server) {
26362
26678
  const schema = {
26363
- kind: optional(stringEnum(["module", "command", "query", "controller", "job", "contract", "resource"]), "[generate] Scaffold kind"),
26679
+ kind: optional(stringEnum(["module", "command", "query", "controller", "job", "contract", "resource", "feature"]), "[generate] Scaffold kind"),
26364
26680
  name: optional(String2(), "[generate] Object name"),
26365
26681
  module: optional(String2(), "[generate] Target feature module"),
26682
+ permission: optional(String2(), "[generate feature] Explicit approval permission; not inferred from name"),
26683
+ rpc: optional(String2(), "[generate feature] Explicit durable RPC capability; must be registered by the host"),
26366
26684
  dir: optional(String2(), "[generate] Feature root directory (default: src/features)"),
26367
- force: optional(Boolean2(), "[generate] Overwrite a single regular file; resource/controller never overwrite"),
26685
+ force: optional(Boolean2(), "[generate] Overwrite a single regular file; resource/feature/controller never overwrite"),
26368
26686
  dry_run: optional(Boolean2(), "[generate] Validate and preview the complete change set without writing"),
26369
26687
  "dry-run": optional(Boolean2(), "[generate] CLI alias of dry_run"),
26370
- register_in: optional(String2(), "[generate] Existing parent module file for module/resource; uses the compiler AST editor"),
26688
+ register_in: optional(String2(), "[generate] Existing parent module file for module/resource/feature; uses the compiler AST editor"),
26371
26689
  "register-in": optional(String2(), "[generate] CLI alias of register_in"),
26372
26690
  root: optional(String2(), "Project directory containing supacloud.config.ts (default: current directory)"),
26373
26691
  include: optional(String2(), "[compile/check] Comma-separated glob patterns for source files"),
@@ -29537,7 +29855,7 @@ function registerDeveloperExperienceTools(server, options = {}) {
29537
29855
  // package.json
29538
29856
  var package_default = {
29539
29857
  name: "@supacloud/cli",
29540
- version: "0.96.2",
29858
+ version: "0.97.0",
29541
29859
  description: "Project-scoped CLI for SupaCloud users",
29542
29860
  type: "module",
29543
29861
  main: "./dist/index.js",
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * An editable vertical feature slice. The generated ports are deliberately
3
+ * fail-closed: database access, authorization and durable command adapters
4
+ * remain application-owned and cannot be fabricated by the scaffold.
5
+ */
6
+ export declare function featureScaffold(name: string, className: string, governance: {
7
+ permission: string;
8
+ rpc: string;
9
+ }): Record<string, string>;
@@ -65,10 +65,12 @@ export interface AppToolArguments {
65
65
  ttl_seconds?: number;
66
66
  wait?: boolean;
67
67
  timeout_seconds?: number;
68
- kind?: "module" | "command" | "query" | "controller" | "job" | "contract" | "resource";
68
+ kind?: "module" | "command" | "query" | "controller" | "job" | "contract" | "resource" | "feature";
69
69
  template?: "minimal" | "http" | "command" | "edge";
70
70
  name?: string;
71
71
  module?: string;
72
+ permission?: string;
73
+ rpc?: string;
72
74
  dir?: string;
73
75
  force?: boolean;
74
76
  dry_run?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.96.2",
3
+ "version": "0.97.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",