@supacloud/cli 0.96.1 → 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
|
@@ -10925,12 +10925,157 @@ function parseDeploymentEvidence(value) {
|
|
|
10925
10925
|
}
|
|
10926
10926
|
return structuredClone(value);
|
|
10927
10927
|
}
|
|
10928
|
-
// node_modules/@supacloud/delivery/src/
|
|
10928
|
+
// node_modules/@supacloud/delivery/src/delivery-tuple.ts
|
|
10929
|
+
var sha2563 = String2({ pattern: "^[a-f0-9]{64}$" });
|
|
10930
|
+
var commit = String2({ pattern: "^(?:[a-f0-9]{40}|[a-f0-9]{64})$" });
|
|
10931
|
+
var isoDate2 = String2({ minLength: 20, maxLength: 40 });
|
|
10932
|
+
var DELIVERY_TUPLE_SCHEMA = "supacloud.fa-delivery-tuple.v1";
|
|
10933
|
+
var FaDeliveryTupleSchema = _Object_({
|
|
10934
|
+
schema: Literal(DELIVERY_TUPLE_SCHEMA),
|
|
10935
|
+
customer: Literal("xigu-fa"),
|
|
10936
|
+
fa_commit: commit,
|
|
10937
|
+
cli: _Object_({
|
|
10938
|
+
version: String2({ pattern: "^\\d+\\.\\d+\\.\\d+$" })
|
|
10939
|
+
}, { additionalProperties: false }),
|
|
10940
|
+
platform_commit: commit,
|
|
10941
|
+
edge_runtime: _Object_({
|
|
10942
|
+
version: String2({ minLength: 1, maxLength: 128 }),
|
|
10943
|
+
artifact_sha256: sha2563
|
|
10944
|
+
}, { additionalProperties: false }),
|
|
10945
|
+
compatibility_window: _Object_({
|
|
10946
|
+
contract_schema: String2({ minLength: 1, maxLength: 128 }),
|
|
10947
|
+
starts_at: isoDate2,
|
|
10948
|
+
ends_at: isoDate2,
|
|
10949
|
+
old_client_supported: Boolean2(),
|
|
10950
|
+
partial_deployment_supported: Boolean2()
|
|
10951
|
+
}, { additionalProperties: false })
|
|
10952
|
+
}, { additionalProperties: false });
|
|
10953
|
+
// node_modules/@supacloud/delivery/src/contract-matrix.ts
|
|
10929
10954
|
var closed6 = { additionalProperties: false };
|
|
10955
|
+
var sha2564 = String2({ pattern: "^[a-f0-9]{64}$" });
|
|
10956
|
+
var generation = Union([Literal("old"), Literal("new")]);
|
|
10957
|
+
var versions = _Object_({ old: sha2564, new: sha2564 }, closed6);
|
|
10958
|
+
var CONTRACT_MATRIX_SCHEMA = "supacloud.consumer-contract-matrix.v1";
|
|
10959
|
+
var FA_CONTRACT_SCENARIOS = [
|
|
10960
|
+
"missing-display-field",
|
|
10961
|
+
"missing-authority-field",
|
|
10962
|
+
"added-field",
|
|
10963
|
+
"legacy-envelope",
|
|
10964
|
+
"lost-write-response",
|
|
10965
|
+
"downgrade"
|
|
10966
|
+
];
|
|
10967
|
+
var ConsumerContractMatrixSchema = _Object_({
|
|
10968
|
+
schema: Literal(CONTRACT_MATRIX_SCHEMA),
|
|
10969
|
+
customer: Literal("xigu-fa"),
|
|
10970
|
+
baseline_tuple_sha256: sha2564,
|
|
10971
|
+
candidate_tuple_sha256: sha2564,
|
|
10972
|
+
artifacts: _Object_({ frontend: versions, functions: versions, database: versions }, closed6),
|
|
10973
|
+
cases: _Array_(_Object_({
|
|
10974
|
+
frontend: generation,
|
|
10975
|
+
functions: generation,
|
|
10976
|
+
database: generation,
|
|
10977
|
+
scenarios: _Array_(_Object_({
|
|
10978
|
+
name: Enum(FA_CONTRACT_SCENARIOS),
|
|
10979
|
+
expected: Union([Literal("compatible"), Literal("blocked-safely")])
|
|
10980
|
+
}, closed6), { minItems: FA_CONTRACT_SCENARIOS.length, maxItems: FA_CONTRACT_SCENARIOS.length })
|
|
10981
|
+
}, closed6), { minItems: 8, maxItems: 8 })
|
|
10982
|
+
}, closed6);
|
|
10983
|
+
// node_modules/@supacloud/delivery/src/fa-stability.ts
|
|
10984
|
+
var closed7 = { additionalProperties: false };
|
|
10985
|
+
var hash3 = String2({ pattern: "^[a-f0-9]{64}$" });
|
|
10986
|
+
var label = String2({ minLength: 1, maxLength: 256, pattern: "\\S" });
|
|
10987
|
+
var timestamp2 = String2({ minLength: 24, maxLength: 24 });
|
|
10988
|
+
var count = Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
10989
|
+
var environment2 = Union([Literal("test"), Literal("production")]);
|
|
10990
|
+
var FA_REQUIRED_ACCEPTANCE = {
|
|
10991
|
+
login: "real-browser",
|
|
10992
|
+
permissions: "real-http",
|
|
10993
|
+
upload: "real-http",
|
|
10994
|
+
"async-task": "real-worker",
|
|
10995
|
+
deploy: "real-http",
|
|
10996
|
+
"database-migration": "real-database",
|
|
10997
|
+
schedule: "real-worker",
|
|
10998
|
+
rollback: "real-drill",
|
|
10999
|
+
"error-policy": "real-http",
|
|
11000
|
+
"alert-delivery": "real-drill"
|
|
11001
|
+
};
|
|
11002
|
+
var FA_BUSINESS_OPERATIONS = ["login", "permissions", "upload", "async-task", "report", "deploy"];
|
|
11003
|
+
var operation = Enum(FA_BUSINESS_OPERATIONS);
|
|
11004
|
+
var proof = _Object_({
|
|
11005
|
+
path: String2({ pattern: "^[A-Za-z0-9_-][A-Za-z0-9_./-]{0,255}$" }),
|
|
11006
|
+
sha256: hash3
|
|
11007
|
+
}, closed7);
|
|
11008
|
+
var FaBusinessObservationSchema = _Object_({
|
|
11009
|
+
id: String2({ pattern: "^[A-Za-z0-9_-]{1,128}$" }),
|
|
11010
|
+
tuple_sha256: hash3,
|
|
11011
|
+
operation,
|
|
11012
|
+
started_at: timestamp2,
|
|
11013
|
+
finished_at: timestamp2,
|
|
11014
|
+
outcome: Enum(["success", "failure", "timeout", "unknown"]),
|
|
11015
|
+
attempts: Integer({ minimum: 1, maximum: 1000 }),
|
|
11016
|
+
rollbacks: count
|
|
11017
|
+
}, closed7);
|
|
11018
|
+
var FaServiceObjectivesSchema = _Object_({
|
|
11019
|
+
status: Literal("approved"),
|
|
11020
|
+
owner: label,
|
|
11021
|
+
approval_reference: label,
|
|
11022
|
+
approved_at: timestamp2,
|
|
11023
|
+
project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
|
|
11024
|
+
environment: environment2,
|
|
11025
|
+
alert_route: label,
|
|
11026
|
+
window_hours: Number2({ exclusiveMinimum: 0, maximum: 8760 }),
|
|
11027
|
+
max_evidence_age_hours: Number2({ exclusiveMinimum: 0, maximum: 168 }),
|
|
11028
|
+
operations: _Array_(_Object_({
|
|
11029
|
+
operation,
|
|
11030
|
+
minimum_samples: Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
|
11031
|
+
availability_percent: Number2({ exclusiveMinimum: 0, maximum: 100 }),
|
|
11032
|
+
p95_ms: Number2({ exclusiveMinimum: 0, maximum: Number.MAX_SAFE_INTEGER })
|
|
11033
|
+
}, closed7), { minItems: FA_BUSINESS_OPERATIONS.length, maxItems: FA_BUSINESS_OPERATIONS.length })
|
|
11034
|
+
}, closed7);
|
|
11035
|
+
var FaStabilityEvidenceSchema = _Object_({
|
|
11036
|
+
schema: Literal("supacloud.fa-stability-evidence.v1"),
|
|
11037
|
+
environment: environment2,
|
|
11038
|
+
project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
|
|
11039
|
+
tuple_sha256: hash3,
|
|
11040
|
+
matrix_sha256: hash3,
|
|
11041
|
+
objectives_sha256: hash3,
|
|
11042
|
+
recorded_at: timestamp2,
|
|
11043
|
+
window_start: timestamp2,
|
|
11044
|
+
window_end: timestamp2,
|
|
11045
|
+
reviewed_by: label,
|
|
11046
|
+
review_reference: label,
|
|
11047
|
+
acceptance: _Array_(_Object_({
|
|
11048
|
+
id: Enum(Object.keys(FA_REQUIRED_ACCEPTANCE)),
|
|
11049
|
+
execution: Enum([
|
|
11050
|
+
"real-browser",
|
|
11051
|
+
"real-http",
|
|
11052
|
+
"real-worker",
|
|
11053
|
+
"real-database",
|
|
11054
|
+
"real-drill",
|
|
11055
|
+
"local-fake"
|
|
11056
|
+
]),
|
|
11057
|
+
passed: Boolean2(),
|
|
11058
|
+
measured_at: timestamp2,
|
|
11059
|
+
proof
|
|
11060
|
+
}, closed7), { minItems: 10, maxItems: 10 }),
|
|
11061
|
+
matrix: _Array_(_Object_({
|
|
11062
|
+
combination: label,
|
|
11063
|
+
scenario: label,
|
|
11064
|
+
outcome: Union([Literal("compatible"), Literal("blocked-safely")]),
|
|
11065
|
+
execution: Union([Literal("real-http-and-database"), Literal("local-fake")]),
|
|
11066
|
+
passed: Boolean2(),
|
|
11067
|
+
measured_at: timestamp2,
|
|
11068
|
+
proof
|
|
11069
|
+
}, closed7), { minItems: 48, maxItems: 48 }),
|
|
11070
|
+
observation: proof,
|
|
11071
|
+
alert: _Object_({ route: label, acknowledged_by: label, delivered_at: timestamp2 }, closed7)
|
|
11072
|
+
}, closed7);
|
|
11073
|
+
// node_modules/@supacloud/delivery/src/application-promotion.ts
|
|
11074
|
+
var closed8 = { additionalProperties: false };
|
|
10930
11075
|
var projectRef2 = String2({ pattern: "^[a-z0-9-]{1,20}$" });
|
|
10931
|
-
var
|
|
11076
|
+
var sha2565 = ApplicationReleaseIdSchema;
|
|
10932
11077
|
var nullableActivation2 = Union([ApplicationActivationIdSchema, Null()]);
|
|
10933
|
-
var nullableDigest = Union([
|
|
11078
|
+
var nullableDigest = Union([sha2565, Null()]);
|
|
10934
11079
|
var observation = {
|
|
10935
11080
|
activation_id: nullableActivation2,
|
|
10936
11081
|
receipt_confirmed: Boolean2(),
|
|
@@ -10956,33 +11101,33 @@ var ApplicationPromotionPlanSchema = _Object_({
|
|
|
10956
11101
|
project_ref: projectRef2,
|
|
10957
11102
|
application_id: ApplicationIdSchema,
|
|
10958
11103
|
environment_id: ApplicationIdSchema,
|
|
10959
|
-
manifest_sha256:
|
|
10960
|
-
plan_sha256:
|
|
11104
|
+
manifest_sha256: sha2565,
|
|
11105
|
+
plan_sha256: sha2565,
|
|
10961
11106
|
source: _Object_({
|
|
10962
11107
|
project_ref: projectRef2,
|
|
10963
11108
|
environment_id: ApplicationIdSchema,
|
|
10964
|
-
release_id:
|
|
11109
|
+
release_id: sha2565,
|
|
10965
11110
|
...observation
|
|
10966
|
-
},
|
|
11111
|
+
}, closed8),
|
|
10967
11112
|
target: _Object_({
|
|
10968
|
-
candidate_release_id:
|
|
10969
|
-
current_release_id: Union([
|
|
11113
|
+
candidate_release_id: sha2565,
|
|
11114
|
+
current_release_id: Union([sha2565, Null()]),
|
|
10970
11115
|
artifact_action: Union([Literal("materialize"), Literal("reuse")]),
|
|
10971
11116
|
configuration_id: Union([ApplicationConfigurationIdSchema, Null()]),
|
|
10972
11117
|
configuration: Union([ApplicationConfigurationViewSchema, Null()]),
|
|
10973
11118
|
...observation
|
|
10974
|
-
},
|
|
11119
|
+
}, closed8),
|
|
10975
11120
|
migrations: _Object_({
|
|
10976
|
-
ledger_digest:
|
|
11121
|
+
ledger_digest: sha2565,
|
|
10977
11122
|
ledger_compatible: Boolean2(),
|
|
10978
11123
|
project_migrations_applied: Boolean2(),
|
|
10979
11124
|
pending_versions: _Array_(String2({ pattern: "^\\d{1,20}$" }), { maxItems: 4096, uniqueItems: true }),
|
|
10980
11125
|
operator_provisioning_required: Boolean2()
|
|
10981
|
-
},
|
|
11126
|
+
}, closed8),
|
|
10982
11127
|
backup: _Object_({
|
|
10983
11128
|
required: Boolean2(),
|
|
10984
11129
|
confirmed: Literal(false)
|
|
10985
|
-
},
|
|
11130
|
+
}, closed8),
|
|
10986
11131
|
action: Union([Literal("promote"), Literal("no-op"), Literal("blocked")]),
|
|
10987
11132
|
blockers: _Array_(Enum([...blocker]), {
|
|
10988
11133
|
maxItems: blocker.length,
|
|
@@ -10991,7 +11136,7 @@ var ApplicationPromotionPlanSchema = _Object_({
|
|
|
10991
11136
|
steps: _Array_(String2({ minLength: 1, maxLength: 64 }), { maxItems: 8, uniqueItems: true }),
|
|
10992
11137
|
execution_performed: Literal(false),
|
|
10993
11138
|
data_recovery: Literal("separate-required")
|
|
10994
|
-
},
|
|
11139
|
+
}, closed8);
|
|
10995
11140
|
function applicationPromotionPlanDigest(value) {
|
|
10996
11141
|
return digest(canonical(value));
|
|
10997
11142
|
}
|
|
@@ -17191,12 +17336,12 @@ async function fetchMutationStatus(http, ref, mutationId) {
|
|
|
17191
17336
|
}
|
|
17192
17337
|
|
|
17193
17338
|
// src/shared/tools/application-workflow-schemas.ts
|
|
17194
|
-
var
|
|
17339
|
+
var closed9 = { additionalProperties: false };
|
|
17195
17340
|
var ref5 = String2({ pattern: "^[A-Za-z0-9_-]{1,20}$" });
|
|
17196
17341
|
var uuid = ApplicationActivationIdSchema2;
|
|
17197
17342
|
var nullableUuid = Union([uuid, Null()]);
|
|
17198
17343
|
var nullableDigest2 = Union([ApplicationReleaseIdSchema2, Null()]);
|
|
17199
|
-
var
|
|
17344
|
+
var timestamp3 = String2({ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" });
|
|
17200
17345
|
var scope2 = { project_ref: ref5, application_id: ApplicationIdSchema2, environment_id: ApplicationIdSchema2 };
|
|
17201
17346
|
var ApplicationPromotionResultSchema = _Object_({
|
|
17202
17347
|
...scope2,
|
|
@@ -17205,7 +17350,7 @@ var ApplicationPromotionResultSchema = _Object_({
|
|
|
17205
17350
|
plan_sha256: ApplicationReleaseIdSchema2,
|
|
17206
17351
|
action: Literal("promote"),
|
|
17207
17352
|
replayed: Boolean2()
|
|
17208
|
-
},
|
|
17353
|
+
}, closed9);
|
|
17209
17354
|
var ApplicationPromotionStatusSchema = _Object_({
|
|
17210
17355
|
...scope2,
|
|
17211
17356
|
mutation: Unknown(),
|
|
@@ -17224,8 +17369,8 @@ var ApplicationPromotionStatusSchema = _Object_({
|
|
|
17224
17369
|
configuration_id: Union([ApplicationConfigurationIdSchema2, Null()]),
|
|
17225
17370
|
backup_id: Union([String2({ pattern: "^logical-full_[a-z0-9-]{1,20}_[a-f0-9]{32}$" }), Null()]),
|
|
17226
17371
|
data_recovery: Literal("separate-required")
|
|
17227
|
-
},
|
|
17228
|
-
},
|
|
17372
|
+
}, closed9)
|
|
17373
|
+
}, closed9);
|
|
17229
17374
|
var phase = Union([Literal("pending"), Literal("ready"), Literal("failed"), Literal("cleaned")]);
|
|
17230
17375
|
var previewId = String2({ pattern: "^[a-f0-9-]{8,64}$" });
|
|
17231
17376
|
var name = String2({ minLength: 1, maxLength: 256 });
|
|
@@ -17235,7 +17380,7 @@ var ApplicationPreviewReceiptSchema2 = _Object_({
|
|
|
17235
17380
|
...scope2,
|
|
17236
17381
|
preview_id: previewId,
|
|
17237
17382
|
release_id: ApplicationReleaseIdSchema2,
|
|
17238
|
-
expires_at: Union([
|
|
17383
|
+
expires_at: Union([timestamp3, Null()]),
|
|
17239
17384
|
status: Union([
|
|
17240
17385
|
Literal("planned"),
|
|
17241
17386
|
Literal("provisioning"),
|
|
@@ -17244,35 +17389,35 @@ var ApplicationPreviewReceiptSchema2 = _Object_({
|
|
|
17244
17389
|
Literal("cleaned")
|
|
17245
17390
|
]),
|
|
17246
17391
|
resources: _Object_({
|
|
17247
|
-
build_artifact: _Object_({ status: phase, release_id: ApplicationReleaseIdSchema2 },
|
|
17392
|
+
build_artifact: _Object_({ status: phase, release_id: ApplicationReleaseIdSchema2 }, closed9),
|
|
17248
17393
|
database_branch: _Object_({
|
|
17249
17394
|
status: phase,
|
|
17250
17395
|
branch_ref: ref5,
|
|
17251
17396
|
data_mode: Union([Literal("schema_only"), Literal("full_clone")])
|
|
17252
|
-
},
|
|
17253
|
-
queue_namespace: _Object_({ status: phase, namespace: name },
|
|
17254
|
-
storage_namespace: _Object_({ status: phase, namespace: name },
|
|
17255
|
-
test_secret: _Object_({ status: phase, name, value_issued: Literal(false) },
|
|
17256
|
-
configuration_revision: _Object_({ status: phase, configuration_id: Union([ApplicationConfigurationIdSchema2, Null()]) },
|
|
17257
|
-
application_activation: _Object_({ status: phase, activation_id: nullableUuid },
|
|
17258
|
-
smoke_test: _Object_({ status: phase, checks, passed: checks, failed: checks },
|
|
17259
|
-
},
|
|
17397
|
+
}, closed9),
|
|
17398
|
+
queue_namespace: _Object_({ status: phase, namespace: name }, closed9),
|
|
17399
|
+
storage_namespace: _Object_({ status: phase, namespace: name }, closed9),
|
|
17400
|
+
test_secret: _Object_({ status: phase, name, value_issued: Literal(false) }, closed9),
|
|
17401
|
+
configuration_revision: _Object_({ status: phase, configuration_id: Union([ApplicationConfigurationIdSchema2, Null()]) }, closed9),
|
|
17402
|
+
application_activation: _Object_({ status: phase, activation_id: nullableUuid }, closed9),
|
|
17403
|
+
smoke_test: _Object_({ status: phase, checks, passed: checks, failed: checks }, closed9)
|
|
17404
|
+
}, closed9),
|
|
17260
17405
|
cleanup: _Object_({
|
|
17261
17406
|
required: Boolean2(),
|
|
17262
17407
|
completed: Boolean2(),
|
|
17263
17408
|
error: Union([String2({ pattern: "^[A-Z][A-Z0-9_]{0,63}$" }), Null()])
|
|
17264
|
-
},
|
|
17409
|
+
}, closed9),
|
|
17265
17410
|
branch_name: Optional(String2({ minLength: 1, maxLength: 80 })),
|
|
17266
17411
|
queue_name: Optional(name),
|
|
17267
17412
|
test_secret_name: Optional(name),
|
|
17268
17413
|
source_configuration_id: Optional(Union([ApplicationConfigurationIdSchema2, Null()])),
|
|
17269
|
-
created_at: Optional(
|
|
17270
|
-
updated_at: Optional(
|
|
17271
|
-
},
|
|
17414
|
+
created_at: Optional(timestamp3),
|
|
17415
|
+
updated_at: Optional(timestamp3)
|
|
17416
|
+
}, closed9);
|
|
17272
17417
|
var previewListSchema = _Object_({
|
|
17273
17418
|
...scope2,
|
|
17274
17419
|
previews: _Array_(ApplicationPreviewReceiptSchema2, { maxItems: 1000 })
|
|
17275
|
-
},
|
|
17420
|
+
}, closed9);
|
|
17276
17421
|
function assertScope(value, identity) {
|
|
17277
17422
|
if (value.project_ref !== identity.project_ref || value.application_id !== identity.application_id || value.environment_id !== identity.environment_id)
|
|
17278
17423
|
throw new Error("Application workflow scope mismatch");
|
|
@@ -17466,14 +17611,14 @@ var actionFields = {
|
|
|
17466
17611
|
reconcile_preview: ["preview_id"],
|
|
17467
17612
|
cleanup_preview: ["preview_id"]
|
|
17468
17613
|
};
|
|
17469
|
-
function
|
|
17614
|
+
function timestamp4(value) {
|
|
17470
17615
|
return Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
17471
17616
|
}
|
|
17472
17617
|
function receipt(value, identity) {
|
|
17473
17618
|
if (!Check2(receiptSchema, value) || value.project_ref !== identity.ref || value.application_id !== identity.id || value.environment_id !== identity.environment || identity.previewId && value.preview_id !== identity.previewId || value.resources.build_artifact.release_id !== value.release_id || value.resources.storage_namespace.namespace !== value.resources.database_branch.branch_ref) {
|
|
17474
17619
|
throw new Error("Invalid preview receipt");
|
|
17475
17620
|
}
|
|
17476
|
-
if (value.expires_at !== undefined && value.expires_at !== null && !
|
|
17621
|
+
if (value.expires_at !== undefined && value.expires_at !== null && !timestamp4(value.expires_at) || value.created_at !== undefined && !timestamp4(value.created_at) || value.updated_at !== undefined && !timestamp4(value.updated_at) || value.expires_at !== undefined && value.expires_at !== null && value.created_at !== undefined && !Check2(ttlSecondsSchema, (Date.parse(value.expires_at) - Date.parse(value.created_at)) / 1000)) {
|
|
17477
17622
|
throw new Error("Invalid preview expiry");
|
|
17478
17623
|
}
|
|
17479
17624
|
if (value.status === "ready") {
|
|
@@ -21200,22 +21345,22 @@ var starter_metadata_default = {
|
|
|
21200
21345
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
21201
21346
|
packages: {
|
|
21202
21347
|
compiler: {
|
|
21203
|
-
version: "0.46.
|
|
21348
|
+
version: "0.46.1"
|
|
21204
21349
|
},
|
|
21205
21350
|
app: {
|
|
21206
|
-
version: "0.
|
|
21351
|
+
version: "0.27.0",
|
|
21207
21352
|
dependencies: {
|
|
21208
21353
|
rxjs: "7.8.2"
|
|
21209
21354
|
}
|
|
21210
21355
|
},
|
|
21211
21356
|
elysia: {
|
|
21212
|
-
version: "0.
|
|
21357
|
+
version: "0.30.0"
|
|
21213
21358
|
},
|
|
21214
21359
|
commands: {
|
|
21215
21360
|
version: "0.9.2"
|
|
21216
21361
|
},
|
|
21217
21362
|
contracts: {
|
|
21218
|
-
version: "0.
|
|
21363
|
+
version: "0.9.0"
|
|
21219
21364
|
},
|
|
21220
21365
|
db: {
|
|
21221
21366
|
version: "0.17.0",
|
|
@@ -24242,6 +24387,302 @@ test("${name} controller preserves an asynchronous read failure", async () => {
|
|
|
24242
24387
|
};
|
|
24243
24388
|
}
|
|
24244
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
|
+
|
|
24245
24686
|
// src/shared/tools/app-local-dev.ts
|
|
24246
24687
|
import { spawn } from "node:child_process";
|
|
24247
24688
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
@@ -24733,6 +25174,12 @@ function requireIdentifier(value, flag) {
|
|
|
24733
25174
|
}
|
|
24734
25175
|
return trimmed;
|
|
24735
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
|
+
}
|
|
24736
25183
|
function moduleScaffold(name) {
|
|
24737
25184
|
return `import { Module } from "@supacloud/app";
|
|
24738
25185
|
|
|
@@ -24751,6 +25198,9 @@ function commandScaffold(moduleName, name) {
|
|
|
24751
25198
|
@Command({
|
|
24752
25199
|
name: ${JSON.stringify(`${moduleName}.${camelName(name)}`)},
|
|
24753
25200
|
permission: ${JSON.stringify(`${moduleName}.${camelName(name)}`)},
|
|
25201
|
+
transaction: "required",
|
|
25202
|
+
audit: ${JSON.stringify(`${moduleName}.${camelName(name)}.completed`)},
|
|
25203
|
+
idempotency: "required",
|
|
24754
25204
|
})
|
|
24755
25205
|
export class ${pascalName(name)}Command {
|
|
24756
25206
|
execute(): void {
|
|
@@ -24805,8 +25255,8 @@ export const ${prefix}Response = t.Object({});
|
|
|
24805
25255
|
}
|
|
24806
25256
|
async function generateScaffold(args) {
|
|
24807
25257
|
const kind = args.kind;
|
|
24808
|
-
if (!kind || !["module", "command", "query", "controller", "job", "contract", "resource"].includes(kind)) {
|
|
24809
|
-
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)");
|
|
24810
25260
|
}
|
|
24811
25261
|
for (const flag of ["dry_run", "force"]) {
|
|
24812
25262
|
if (args[flag] !== undefined && typeof args[flag] !== "boolean") {
|
|
@@ -24817,24 +25267,31 @@ async function generateScaffold(args) {
|
|
|
24817
25267
|
const dir = args.dir ?? "src/features";
|
|
24818
25268
|
if (dir !== ".")
|
|
24819
25269
|
scaffoldPath(root, dir);
|
|
24820
|
-
if (args.register_in !== undefined && kind !== "resource" && kind !== "module") {
|
|
24821
|
-
throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", "--register-in is only supported for a new module or
|
|
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`);
|
|
24822
25275
|
}
|
|
24823
|
-
if (kind
|
|
24824
|
-
throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", "
|
|
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");
|
|
24825
25278
|
}
|
|
25279
|
+
const featureGovernance = kind === "feature" ? {
|
|
25280
|
+
permission: requireFeatureCapability(args.permission, "permission"),
|
|
25281
|
+
rpc: requireFeatureCapability(args.rpc, "rpc")
|
|
25282
|
+
} : undefined;
|
|
24826
25283
|
const writes = [];
|
|
24827
25284
|
let modulePath;
|
|
24828
25285
|
let moduleSymbol;
|
|
24829
25286
|
let contractName;
|
|
24830
|
-
if (kind === "module" || kind === "resource") {
|
|
25287
|
+
if (kind === "module" || kind === "resource" || kind === "feature") {
|
|
24831
25288
|
const name = requireIdentifier(args.name, "name");
|
|
24832
|
-
modulePath = join13(dir, name, `${name}.module.ts`).replaceAll("\\", "/");
|
|
24833
|
-
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`;
|
|
24834
25291
|
if (kind === "module")
|
|
24835
25292
|
writes.push({ path: modulePath, content: moduleScaffold(name), overwrite: args.force === true });
|
|
24836
25293
|
else
|
|
24837
|
-
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)))) {
|
|
24838
25295
|
writes.push({ path: join13(dir, name, file).replaceAll("\\", "/"), content });
|
|
24839
25296
|
}
|
|
24840
25297
|
} else {
|
|
@@ -24896,6 +25353,8 @@ async function generateScaffold(args) {
|
|
|
24896
25353
|
lines.push("No files written. Omit --dry-run to apply this generation.");
|
|
24897
25354
|
if (kind === "resource")
|
|
24898
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.");
|
|
24899
25358
|
if (contractName) {
|
|
24900
25359
|
const prefix = pascalName(contractName);
|
|
24901
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.");
|
|
@@ -26185,15 +26644,17 @@ function registerAppTools(server, options = {}) {
|
|
|
26185
26644
|
"promote-status",
|
|
26186
26645
|
"promote-reconcile"
|
|
26187
26646
|
]), "App action; promote obtains and verifies its plan before one mutation request"),
|
|
26188
|
-
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"),
|
|
26189
26648
|
template: optional(stringEnum(["minimal", "http", "command", "edge"]), "[init] Minimal application by default; explicit http/command/edge recipes"),
|
|
26190
26649
|
name: optional(String2(), "[init/generate] Project or object name"),
|
|
26191
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"),
|
|
26192
26653
|
dir: optional(String2(), "[generate] Feature root directory (default: src/features)"),
|
|
26193
|
-
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"),
|
|
26194
26655
|
dry_run: optional(Boolean2(), "[generate] Validate and preview the complete change set without writing"),
|
|
26195
26656
|
"dry-run": optional(Boolean2(), "[generate] CLI alias of dry_run"),
|
|
26196
|
-
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"),
|
|
26197
26658
|
"register-in": optional(String2(), "[generate] CLI alias of register_in"),
|
|
26198
26659
|
root: optional(String2(), "[dev/plan/build/release/compile/check] Project directory containing supacloud.config.ts (default: current directory)"),
|
|
26199
26660
|
include: optional(String2(), "[dev/compile/check] Comma-separated glob patterns for source files"),
|
|
@@ -26215,14 +26676,16 @@ function registerAppTools(server, options = {}) {
|
|
|
26215
26676
|
var APP_ALIAS_ACTIONS = ["generate", "compile", "check", "graph", "explain", "context", "doctor", "fix"];
|
|
26216
26677
|
function registerAppAliases(server) {
|
|
26217
26678
|
const schema = {
|
|
26218
|
-
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"),
|
|
26219
26680
|
name: optional(String2(), "[generate] Object name"),
|
|
26220
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"),
|
|
26221
26684
|
dir: optional(String2(), "[generate] Feature root directory (default: src/features)"),
|
|
26222
|
-
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"),
|
|
26223
26686
|
dry_run: optional(Boolean2(), "[generate] Validate and preview the complete change set without writing"),
|
|
26224
26687
|
"dry-run": optional(Boolean2(), "[generate] CLI alias of dry_run"),
|
|
26225
|
-
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"),
|
|
26226
26689
|
"register-in": optional(String2(), "[generate] CLI alias of register_in"),
|
|
26227
26690
|
root: optional(String2(), "Project directory containing supacloud.config.ts (default: current directory)"),
|
|
26228
26691
|
include: optional(String2(), "[compile/check] Comma-separated glob patterns for source files"),
|
|
@@ -26262,7 +26725,7 @@ import {
|
|
|
26262
26725
|
reverseDatabase
|
|
26263
26726
|
} from "@supacloud/db";
|
|
26264
26727
|
import { parseDatabaseSourcesConfig } from "@supacloud/db/source-contracts";
|
|
26265
|
-
var
|
|
26728
|
+
var hash4 = (content) => createHash7("sha256").update(content).digest("hex");
|
|
26266
26729
|
var result = (value, isError = false) => ({
|
|
26267
26730
|
isError,
|
|
26268
26731
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
@@ -26393,7 +26856,7 @@ async function initializeCandidate(stage, majorVersion = 17) {
|
|
|
26393
26856
|
await mkdir6(join14(stage, "supabase/migrations"), { recursive: true });
|
|
26394
26857
|
await mkdir6(join14(stage, "supabase/schemas"), { recursive: true });
|
|
26395
26858
|
await writeFile5(join14(stage, "supabase/config.toml"), [
|
|
26396
|
-
`project_id = "supacloud-candidate-${
|
|
26859
|
+
`project_id = "supacloud-candidate-${hash4(stage).slice(0, 12)}"`,
|
|
26397
26860
|
"[db]",
|
|
26398
26861
|
`major_version = ${majorVersion}`,
|
|
26399
26862
|
"[db.migrations]",
|
|
@@ -26521,7 +26984,7 @@ async function migrations(root, path) {
|
|
|
26521
26984
|
const files = [];
|
|
26522
26985
|
for (const file of names) {
|
|
26523
26986
|
const text = await readFile5(await projectPath2(root, `${path}/${file}`), "utf8");
|
|
26524
|
-
files.push({ file, text, sha256:
|
|
26987
|
+
files.push({ file, text, sha256: hash4(text) });
|
|
26525
26988
|
}
|
|
26526
26989
|
return { dir, files };
|
|
26527
26990
|
}
|
|
@@ -26551,7 +27014,7 @@ async function migrationAction(args, root, options) {
|
|
|
26551
27014
|
files: input.files.map(({ file, sha256 }) => ({ file, sha256 })),
|
|
26552
27015
|
dryRun
|
|
26553
27016
|
};
|
|
26554
|
-
const digest =
|
|
27017
|
+
const digest = hash4(JSON.stringify(report));
|
|
26555
27018
|
if (dryRun.isError)
|
|
26556
27019
|
return result({ ...report, ok: false, digest, executionPerformed: false }, true);
|
|
26557
27020
|
if (args.action === "plan")
|
|
@@ -28168,7 +28631,7 @@ async function requireProjectDeploymentProtocol(http, projectRef, environmentId)
|
|
|
28168
28631
|
throw new Error("DEPLOYMENT_PROTOCOL_MISMATCH: Upgrade the SupaCloud server and CLI together (`npm install -g @supacloud/cli@latest`), then retry `supacloud deploy`. No upload or deployment was attempted; legacy endpoints will not be used.");
|
|
28169
28632
|
}
|
|
28170
28633
|
}
|
|
28171
|
-
var
|
|
28634
|
+
var operation2 = "applications.project_deploy";
|
|
28172
28635
|
var scopeSchema = _Object_({
|
|
28173
28636
|
project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
|
|
28174
28637
|
application_id: ApplicationIdSchema,
|
|
@@ -28217,7 +28680,7 @@ async function runProjectDeployment(http, args) {
|
|
|
28217
28680
|
...args.web_release === undefined ? {} : { web_release: args.web_release }
|
|
28218
28681
|
};
|
|
28219
28682
|
if (!Check2(scopeSchema, scope) || !Check2(ApplicationDeployRequestSchema, request) || request.configuration_id !== undefined && request.configuration_write !== undefined && request.configuration_id !== request.configuration_write.configuration_id) {
|
|
28220
|
-
return releaseControlFailure(
|
|
28683
|
+
return releaseControlFailure(operation2, "INVALID_RESPONSE", null);
|
|
28221
28684
|
}
|
|
28222
28685
|
const expected = { ...scope, request };
|
|
28223
28686
|
const identity = {
|
|
@@ -28235,13 +28698,13 @@ async function runProjectDeployment(http, args) {
|
|
|
28235
28698
|
}, { timeoutMs: 120000, maxJsonBytes: 131072, responseTimeoutMs: 30000 });
|
|
28236
28699
|
if (result.ok && result.status === 200) {
|
|
28237
28700
|
try {
|
|
28238
|
-
return releaseControlSuccess(
|
|
28701
|
+
return releaseControlSuccess(operation2, {
|
|
28239
28702
|
...responseReceipt(result.data, expected),
|
|
28240
28703
|
deployment_id: mutation_id
|
|
28241
28704
|
});
|
|
28242
28705
|
} catch {}
|
|
28243
28706
|
} else if (!result.ok && !result.transportError && !result.responseReadError && result.status < 500 && result.status !== 408) {
|
|
28244
|
-
return releaseControlMutationFailure(
|
|
28707
|
+
return releaseControlMutationFailure(operation2, result, identity);
|
|
28245
28708
|
}
|
|
28246
28709
|
const observed = await http.get(`${endpoint}/${encodeURIComponent(mutation_id)}`, {
|
|
28247
28710
|
retry: false,
|
|
@@ -28255,14 +28718,14 @@ async function runProjectDeployment(http, args) {
|
|
|
28255
28718
|
if (state.deployment_id !== mutation_id || state.project_ref !== scope.project_ref || state.application_id !== scope.application_id || state.environment_id !== scope.environment_id || state.status !== "succeeded" || state.phase !== "verifying")
|
|
28256
28719
|
throw new Error;
|
|
28257
28720
|
const receipt = parseApplicationDeployResult(state.receipt, expected);
|
|
28258
|
-
return releaseControlSuccess(
|
|
28721
|
+
return releaseControlSuccess(operation2, {
|
|
28259
28722
|
...receipt,
|
|
28260
28723
|
deployment_id: mutation_id,
|
|
28261
28724
|
replayed: true,
|
|
28262
28725
|
execution_performed: false
|
|
28263
28726
|
});
|
|
28264
28727
|
} catch {
|
|
28265
|
-
return releaseControlFailure(
|
|
28728
|
+
return releaseControlFailure(operation2, "OUTCOME_UNKNOWN", result.transportError ? null : result.status, identity);
|
|
28266
28729
|
}
|
|
28267
28730
|
}
|
|
28268
28731
|
|
|
@@ -29392,7 +29855,7 @@ function registerDeveloperExperienceTools(server, options = {}) {
|
|
|
29392
29855
|
// package.json
|
|
29393
29856
|
var package_default = {
|
|
29394
29857
|
name: "@supacloud/cli",
|
|
29395
|
-
version: "0.
|
|
29858
|
+
version: "0.97.0",
|
|
29396
29859
|
description: "Project-scoped CLI for SupaCloud users",
|
|
29397
29860
|
type: "module",
|
|
29398
29861
|
main: "./dist/index.js",
|
|
@@ -29444,14 +29907,14 @@ var package_default = {
|
|
|
29444
29907
|
directory: "packages/cli"
|
|
29445
29908
|
},
|
|
29446
29909
|
dependencies: {
|
|
29447
|
-
"@supacloud/compiler": "^0.46.
|
|
29448
|
-
"@supacloud/delivery": "0.
|
|
29910
|
+
"@supacloud/compiler": "^0.46.1",
|
|
29911
|
+
"@supacloud/delivery": "0.18.0",
|
|
29449
29912
|
"@supacloud/db": "0.17.0",
|
|
29450
29913
|
typebox: "1.3.36"
|
|
29451
29914
|
},
|
|
29452
29915
|
overrides: {
|
|
29453
|
-
"@supacloud/contracts": "0.
|
|
29454
|
-
"@supacloud/delivery": "0.
|
|
29916
|
+
"@supacloud/contracts": "0.9.0",
|
|
29917
|
+
"@supacloud/delivery": "0.18.0",
|
|
29455
29918
|
"@graphql-tools/utils": "^12.0.3"
|
|
29456
29919
|
},
|
|
29457
29920
|
devDependencies: {
|
|
@@ -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.
|
|
3
|
+
"version": "0.97.0",
|
|
4
4
|
"description": "Project-scoped CLI for SupaCloud users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
"directory": "packages/cli"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@supacloud/compiler": "^0.46.
|
|
56
|
-
"@supacloud/delivery": "0.
|
|
55
|
+
"@supacloud/compiler": "^0.46.1",
|
|
56
|
+
"@supacloud/delivery": "0.18.0",
|
|
57
57
|
"@supacloud/db": "0.17.0",
|
|
58
58
|
"typebox": "1.3.36"
|
|
59
59
|
},
|
|
60
60
|
"overrides": {
|
|
61
|
-
"@supacloud/contracts": "0.
|
|
62
|
-
"@supacloud/delivery": "0.
|
|
61
|
+
"@supacloud/contracts": "0.9.0",
|
|
62
|
+
"@supacloud/delivery": "0.18.0",
|
|
63
63
|
"@graphql-tools/utils": "^12.0.3"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|