@sleepy-hollow/framework 0.3.0 → 0.3.2
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/CHANGELOG.md +24 -0
- package/README.md +6 -6
- package/dist/cli.d.ts +1 -103
- package/dist/cli.js +409 -532
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-D4U3ZY4O.js";
|
|
9
9
|
import "./chunk-BAKXP7IR.js";
|
|
10
10
|
import {
|
|
11
|
-
composeProjectSecurity
|
|
12
|
-
redactSecurityData
|
|
11
|
+
composeProjectSecurity
|
|
13
12
|
} from "./chunk-DGTHFZPZ.js";
|
|
14
13
|
import {
|
|
15
14
|
normalizeRoutes
|
|
@@ -22,10 +21,11 @@ import "./chunk-LNJDFJGT.js";
|
|
|
22
21
|
import "./chunk-5WRI5ZAA.js";
|
|
23
22
|
|
|
24
23
|
// cli/main.ts
|
|
24
|
+
import { realpathSync } from "fs";
|
|
25
25
|
import { pathToFileURL as pathToFileURL4 } from "url";
|
|
26
26
|
|
|
27
27
|
// cli/adapters.ts
|
|
28
|
-
import { isAbsolute as isAbsolute5, join as
|
|
28
|
+
import { isAbsolute as isAbsolute5, join as join4, relative as relative5, sep as sep6 } from "path";
|
|
29
29
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
30
30
|
|
|
31
31
|
// cli/check/render.ts
|
|
@@ -652,13 +652,13 @@ function verify(inventory2) {
|
|
|
652
652
|
|
|
653
653
|
// cli/check/command.ts
|
|
654
654
|
function parse(args) {
|
|
655
|
-
let
|
|
655
|
+
let json2 = false;
|
|
656
656
|
let scope = { kind: "full" };
|
|
657
657
|
let scopeSeen = false;
|
|
658
658
|
for (let index = 0; index < args.length; index++) {
|
|
659
659
|
const argument = args[index];
|
|
660
660
|
if (argument === "--json") {
|
|
661
|
-
|
|
661
|
+
json2 = true;
|
|
662
662
|
continue;
|
|
663
663
|
}
|
|
664
664
|
if (argument === "--full") {
|
|
@@ -699,7 +699,7 @@ function parse(args) {
|
|
|
699
699
|
if (scope.kind === "route" && (!/^[A-Z]+$/.test(scope.method) || !scope.path.startsWith("/") || scope.path.includes(".."))) {
|
|
700
700
|
throw new TypeError("Route scope uses an unsafe method or path");
|
|
701
701
|
}
|
|
702
|
-
return { json:
|
|
702
|
+
return { json: json2, scope };
|
|
703
703
|
}
|
|
704
704
|
function safeMessage(error) {
|
|
705
705
|
return error instanceof Error ? error.message.slice(0, 500) : "Verification evidence could not be collected";
|
|
@@ -753,299 +753,245 @@ async function runCheckCommand(args, io, load) {
|
|
|
753
753
|
var MAX_OUTPUT = 1024 * 1024;
|
|
754
754
|
var MAX_TIMEOUT = 10 * 60 * 1e3;
|
|
755
755
|
|
|
756
|
-
// cli/
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
var
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
const token = raw.trim();
|
|
771
|
-
if (/\s/.test(token)) {
|
|
772
|
-
throw new Error(`${TOKEN_VARIABLE} must be a single token with no whitespace.`);
|
|
756
|
+
// cli/deploy/prepare.ts
|
|
757
|
+
import { createHash } from "crypto";
|
|
758
|
+
import { join } from "path";
|
|
759
|
+
var OWNER = "# Generated by Sleepy Hollow. Managed deployment artifact.";
|
|
760
|
+
var APP = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
761
|
+
var REGION = /^[a-z]{3}$/;
|
|
762
|
+
var DeploymentPreparationError = class extends Error {
|
|
763
|
+
diagnostics;
|
|
764
|
+
constructor(diagnostics) {
|
|
765
|
+
super(diagnostics[0]?.summary ?? "Deployment preparation failed.");
|
|
766
|
+
this.name = "DeploymentPreparationError";
|
|
767
|
+
this.diagnostics = diagnostics;
|
|
773
768
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
return { id, status, observedStatus, evidence };
|
|
778
|
-
}
|
|
779
|
-
function flyAdapter(options) {
|
|
780
|
-
return {
|
|
781
|
-
async upload(request) {
|
|
782
|
-
const result = await options.runner.run({
|
|
783
|
-
command: ["flyctl", "deploy", "--app", request.target.project, "--remote-only"],
|
|
784
|
-
environment: { [TOKEN_VARIABLE]: request.token }
|
|
785
|
-
});
|
|
786
|
-
const url = `https://${request.target.project}.fly.dev`;
|
|
787
|
-
return {
|
|
788
|
-
url,
|
|
789
|
-
revision: result.stdout.trim() || request.revision
|
|
790
|
-
};
|
|
791
|
-
},
|
|
792
|
-
async health(request) {
|
|
793
|
-
try {
|
|
794
|
-
const response = await options.transport(new URL("/", request.url));
|
|
795
|
-
return outcome("HEALTH", response.ok ? "passed" : "failed", response.status, `GET / returned ${response.status}`);
|
|
796
|
-
} catch (error) {
|
|
797
|
-
return outcome("HEALTH", "failed", 0, `GET / failed: ${error instanceof Error ? error.message : "unknown transport error"}`);
|
|
798
|
-
}
|
|
799
|
-
},
|
|
800
|
-
async smoke(request) {
|
|
801
|
-
try {
|
|
802
|
-
const response = await options.transport(new URL(request.test.path, request.url), { method: request.test.method });
|
|
803
|
-
return outcome(request.test.id, response.status === request.test.expectedStatus ? "passed" : "failed", response.status, `${request.test.method} ${request.test.path} returned ${response.status}, expected ${request.test.expectedStatus}`);
|
|
804
|
-
} catch (error) {
|
|
805
|
-
return outcome(request.test.id, "failed", 0, `${request.test.method} ${request.test.path} failed: ${error instanceof Error ? error.message : "unknown transport error"}`);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
};
|
|
769
|
+
};
|
|
770
|
+
function failure(code, summary, correction) {
|
|
771
|
+
throw new DeploymentPreparationError([{ code, summary, correction }]);
|
|
809
772
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
function plan(inventory2) {
|
|
813
|
-
const deployed = new Set(inventory2.deployedEnvironmentKeys);
|
|
814
|
-
const current = new Set(inventory2.environmentKeys);
|
|
815
|
-
const environmentKeyChanges = [
|
|
816
|
-
...inventory2.environmentKeys.filter((key) => !deployed.has(key)).map((key) => ({ key, change: "added" })),
|
|
817
|
-
...inventory2.deployedEnvironmentKeys.filter((key) => !current.has(key)).map(
|
|
818
|
-
(key) => ({ key, change: "removed" })
|
|
819
|
-
)
|
|
820
|
-
].sort((left, right) => left.key.localeCompare(right.key));
|
|
821
|
-
const breaking = inventory2.contractChanges.some(
|
|
822
|
-
(change) => change.severity === "breaking"
|
|
823
|
-
);
|
|
824
|
-
const unchanged = inventory2.deployedRevision === inventory2.revision && environmentKeyChanges.length === 0 && inventory2.contractChanges.length === 0;
|
|
825
|
-
return {
|
|
826
|
-
target: inventory2.target,
|
|
827
|
-
revision: inventory2.revision,
|
|
828
|
-
...inventory2.deployedRevision ? { deployedRevision: inventory2.deployedRevision } : {},
|
|
829
|
-
environmentKeyChanges,
|
|
830
|
-
contractChanges: [...inventory2.contractChanges],
|
|
831
|
-
smokeTests: [...inventory2.smokeTests],
|
|
832
|
-
requiresConfirmation: !unchanged && (inventory2.firstExternalDeployment || breaking),
|
|
833
|
-
unchanged
|
|
834
|
-
};
|
|
773
|
+
function digest(content) {
|
|
774
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
835
775
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
var DEPLOY_TARGET_KINDS = ["fly"];
|
|
839
|
-
|
|
840
|
-
// cli/deploy/deployment.ts
|
|
841
|
-
function blocked(request, built, diagnostics) {
|
|
842
|
-
return {
|
|
843
|
-
schema: "sleepy-hollow-deploy-result/v1",
|
|
844
|
-
ok: false,
|
|
845
|
-
command: "deploy",
|
|
846
|
-
projectRoot: request.inventory.projectRootDisplay,
|
|
847
|
-
outcome: "blocked",
|
|
848
|
-
plan: built,
|
|
849
|
-
smokeResults: [],
|
|
850
|
-
diagnostics
|
|
851
|
-
};
|
|
776
|
+
function quote(value) {
|
|
777
|
+
return JSON.stringify(value);
|
|
852
778
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (!DEPLOY_TARGET_KINDS.includes(inventory2.target.kind)) {
|
|
857
|
-
return blocked(request, built, [{
|
|
858
|
-
code: "SH_DEPLOY_TARGET_UNSUPPORTED",
|
|
859
|
-
severity: "error",
|
|
860
|
-
summary: `Deployment target ${inventory2.target.kind} is not supported in this release.`,
|
|
861
|
-
evidence: [`requested target: ${inventory2.target.kind}`],
|
|
862
|
-
correction: `Deploy to a supported target: ${DEPLOY_TARGET_KINDS.join(", ")}.`
|
|
863
|
-
}]);
|
|
779
|
+
function normalize(request) {
|
|
780
|
+
if (request.target.kind !== "fly") {
|
|
781
|
+
return failure("SH_DEPLOY_TARGET_UNSUPPORTED", "Only Fly deployment preparation is supported.", "Use --target fly:<app>.");
|
|
864
782
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
return
|
|
868
|
-
code: "SH_DEPLOY_VERIFICATION_FAILED",
|
|
869
|
-
severity: "error",
|
|
870
|
-
summary: "Required verification failed, so nothing was uploaded.",
|
|
871
|
-
evidence: evidence.length > 0 ? evidence : [
|
|
872
|
-
`hollow check reported ${inventory2.verification.summary.failed} failed checks`
|
|
873
|
-
],
|
|
874
|
-
correction: "Resolve every reported diagnostic and rerun hollow check."
|
|
875
|
-
}]);
|
|
783
|
+
const app = request.target.app.trim();
|
|
784
|
+
if (!APP.test(app)) {
|
|
785
|
+
return failure("SH_DEPLOY_TARGET_INVALID", "The Fly app name is invalid.", "Use a Fly app name containing lowercase letters, numbers, and hyphens.");
|
|
876
786
|
}
|
|
877
|
-
if (
|
|
878
|
-
return
|
|
879
|
-
schema: "sleepy-hollow-deploy-result/v1",
|
|
880
|
-
ok: true,
|
|
881
|
-
command: "deploy",
|
|
882
|
-
projectRoot: inventory2.projectRootDisplay,
|
|
883
|
-
outcome: "unchanged",
|
|
884
|
-
plan: built,
|
|
885
|
-
deployedRevision: inventory2.revision,
|
|
886
|
-
openApiPath: inventory2.openApiPath,
|
|
887
|
-
documentationPath: inventory2.documentationPath,
|
|
888
|
-
smokeResults: [],
|
|
889
|
-
completedAt: now(),
|
|
890
|
-
diagnostics: []
|
|
891
|
-
};
|
|
787
|
+
if (request.database !== "sqlite" && request.database !== "postgres") {
|
|
788
|
+
return failure("SH_DEPLOY_DATABASE_INVALID", "The deployment database profile is invalid.", "Use --database sqlite or --database postgres.");
|
|
892
789
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
ok: false,
|
|
897
|
-
command: "deploy",
|
|
898
|
-
projectRoot: inventory2.projectRootDisplay,
|
|
899
|
-
outcome: "confirmation-required",
|
|
900
|
-
plan: built,
|
|
901
|
-
smokeResults: [],
|
|
902
|
-
diagnostics: [{
|
|
903
|
-
code: "SH_DEPLOY_CONFIRMATION_REQUIRED",
|
|
904
|
-
severity: "error",
|
|
905
|
-
summary: "The first external deployment or a materially risky change requires explicit confirmation.",
|
|
906
|
-
evidence: [
|
|
907
|
-
`target: ${inventory2.target.kind}:${inventory2.target.project}`,
|
|
908
|
-
`revision: ${inventory2.revision}`
|
|
909
|
-
],
|
|
910
|
-
correction: "Review the deployment plan and confirm before deploying."
|
|
911
|
-
}]
|
|
912
|
-
};
|
|
790
|
+
const region = request.region?.trim();
|
|
791
|
+
if (request.database === "sqlite" && !region) {
|
|
792
|
+
return failure("SH_DEPLOY_REGION_REQUIRED", "SQLite preparation requires a Fly region for its data volume.", "Add --region <three-letter-region>.");
|
|
913
793
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
revision: inventory2.revision,
|
|
917
|
-
token: request.token
|
|
918
|
-
});
|
|
919
|
-
const health = await adapter.health({ url: upload.url });
|
|
920
|
-
const smokeResults = [];
|
|
921
|
-
for (const test of inventory2.smokeTests) {
|
|
922
|
-
smokeResults.push(await adapter.smoke({ url: upload.url, test }));
|
|
794
|
+
if (region && !REGION.test(region)) {
|
|
795
|
+
return failure("SH_DEPLOY_REGION_INVALID", "The Fly region must be a three-letter region code.", "Use a valid Fly region, such as iad.");
|
|
923
796
|
}
|
|
924
|
-
const requiredFailures = smokeResults.filter(
|
|
925
|
-
(outcome2, index) => outcome2.status === "failed" && inventory2.smokeTests[index].required
|
|
926
|
-
);
|
|
927
|
-
const failed = health.status === "failed" || requiredFailures.length > 0;
|
|
928
797
|
return {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
plan: built,
|
|
935
|
-
url: upload.url,
|
|
936
|
-
deployedRevision: upload.revision,
|
|
937
|
-
openApiPath: inventory2.openApiPath,
|
|
938
|
-
documentationPath: inventory2.documentationPath,
|
|
939
|
-
health,
|
|
940
|
-
smokeResults,
|
|
941
|
-
completedAt: now(),
|
|
942
|
-
diagnostics: failed ? [{
|
|
943
|
-
code: "SH_DEPLOY_SMOKE_FAILED",
|
|
944
|
-
severity: "error",
|
|
945
|
-
summary: "The deployment is live but a required smoke test failed. It is not a successful deployment.",
|
|
946
|
-
evidence: [
|
|
947
|
-
`live revision: ${upload.revision}`,
|
|
948
|
-
...health.status === "failed" ? [`health: ${health.evidence}`] : [],
|
|
949
|
-
...requiredFailures.map(
|
|
950
|
-
(outcome2) => `${outcome2.id}: ${outcome2.evidence}`
|
|
951
|
-
)
|
|
952
|
-
],
|
|
953
|
-
correction: "Investigate the live revision, then repair and redeploy or roll back."
|
|
954
|
-
}] : []
|
|
798
|
+
projectRoot: request.projectRoot,
|
|
799
|
+
target: { kind: "fly", app },
|
|
800
|
+
database: request.database,
|
|
801
|
+
...region ? { region } : {},
|
|
802
|
+
...request.force ? { force: true } : {}
|
|
955
803
|
};
|
|
956
804
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
function human2(result) {
|
|
967
|
-
const lines = [];
|
|
968
|
-
const { plan: plan3 } = result;
|
|
969
|
-
lines.push(
|
|
970
|
-
`${outcomes[result.outcome]}: ${plan3.target.kind}:${plan3.target.project}`
|
|
971
|
-
);
|
|
972
|
-
lines.push(` revision ${plan3.revision}`);
|
|
973
|
-
if (plan3.deployedRevision) {
|
|
974
|
-
lines.push(` previously ${plan3.deployedRevision}`);
|
|
975
|
-
}
|
|
976
|
-
if (result.url) lines.push(` url ${result.url}`);
|
|
977
|
-
if (result.deployedRevision) {
|
|
978
|
-
lines.push(` live revision ${result.deployedRevision}`);
|
|
979
|
-
}
|
|
980
|
-
if (result.openApiPath) {
|
|
981
|
-
lines.push(` openapi ${result.openApiPath}`);
|
|
982
|
-
}
|
|
983
|
-
if (result.documentationPath) {
|
|
984
|
-
lines.push(` documentation ${result.documentationPath}`);
|
|
985
|
-
}
|
|
986
|
-
if (result.completedAt) {
|
|
987
|
-
lines.push(` completed ${result.completedAt}`);
|
|
988
|
-
}
|
|
989
|
-
for (const change of plan3.environmentKeyChanges) {
|
|
990
|
-
lines.push(` env ${change.change} ${change.key}`);
|
|
991
|
-
}
|
|
992
|
-
for (const change of plan3.contractChanges) {
|
|
993
|
-
lines.push(
|
|
994
|
-
` contract ${change.severity} ${change.code} ${change.operationId}`
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
if (result.health) {
|
|
998
|
-
lines.push(
|
|
999
|
-
` health ${result.health.status} ${result.health.evidence}`
|
|
1000
|
-
);
|
|
805
|
+
async function runtime(projectRoot) {
|
|
806
|
+
let manifest;
|
|
807
|
+
try {
|
|
808
|
+
manifest = JSON.parse(await platform.readTextFile(join(projectRoot, "package.json")));
|
|
809
|
+
} catch (error) {
|
|
810
|
+
if (platform.isNotFound(error)) {
|
|
811
|
+
return failure("SH_DEPLOY_PACKAGE_MISSING", "A deployable project must contain package.json.", "Create a Node or Bun package manifest with a start script.");
|
|
812
|
+
}
|
|
813
|
+
return failure("SH_DEPLOY_PACKAGE_INVALID", "package.json could not be read as JSON.", "Repair package.json and include a start script.");
|
|
1001
814
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
` smoke ${outcome2.id} ${outcome2.status} ${outcome2.evidence}`
|
|
1005
|
-
);
|
|
815
|
+
if (typeof manifest.scripts?.start !== "string" || manifest.scripts.start.trim().length === 0) {
|
|
816
|
+
return failure("SH_DEPLOY_START_MISSING", "A deployable project must declare a non-empty start script.", "Add scripts.start to package.json before preparing deployment.");
|
|
1006
817
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
818
|
+
return typeof manifest.packageManager === "string" && manifest.packageManager.startsWith("bun@") ? "bun" : "node";
|
|
819
|
+
}
|
|
820
|
+
function dockerfile(runtime2) {
|
|
821
|
+
if (runtime2 === "bun") {
|
|
822
|
+
return `${OWNER}
|
|
823
|
+
FROM oven/bun:1.3.14
|
|
824
|
+
|
|
825
|
+
WORKDIR /app
|
|
826
|
+
COPY package.json ./
|
|
827
|
+
RUN bun install --production
|
|
828
|
+
COPY . .
|
|
829
|
+
|
|
830
|
+
ENV NODE_ENV=production
|
|
831
|
+
ENV PORT=3000
|
|
832
|
+
EXPOSE 3000
|
|
833
|
+
CMD ["bun", "run", "start"]
|
|
834
|
+
`;
|
|
1015
835
|
}
|
|
1016
|
-
return `${
|
|
836
|
+
return `${OWNER}
|
|
837
|
+
FROM node:24-bookworm-slim
|
|
838
|
+
|
|
839
|
+
WORKDIR /app
|
|
840
|
+
COPY package.json ./
|
|
841
|
+
RUN npm install --omit=dev
|
|
842
|
+
COPY . .
|
|
843
|
+
|
|
844
|
+
ENV NODE_ENV=production
|
|
845
|
+
ENV PORT=3000
|
|
846
|
+
EXPOSE 3000
|
|
847
|
+
CMD ["npm", "run", "start"]
|
|
1017
848
|
`;
|
|
1018
849
|
}
|
|
1019
|
-
function
|
|
1020
|
-
return `${
|
|
850
|
+
function dockerignore() {
|
|
851
|
+
return `${OWNER}
|
|
852
|
+
.git
|
|
853
|
+
.gitignore
|
|
854
|
+
node_modules
|
|
855
|
+
.env
|
|
856
|
+
.env.*
|
|
857
|
+
!.env.example
|
|
858
|
+
*.db
|
|
859
|
+
*.sqlite
|
|
860
|
+
*.sqlite3
|
|
861
|
+
data
|
|
862
|
+
coverage
|
|
863
|
+
generated/capture.json
|
|
1021
864
|
`;
|
|
1022
865
|
}
|
|
866
|
+
function flyToml(request) {
|
|
867
|
+
const shared = `${OWNER}
|
|
868
|
+
app = ${quote(request.target.app)}
|
|
869
|
+
${request.region ? `primary_region = ${quote(request.region)}
|
|
870
|
+
` : ""}
|
|
871
|
+
[http_service]
|
|
872
|
+
internal_port = 3000
|
|
873
|
+
force_https = true
|
|
874
|
+
auto_stop_machines = "stop"
|
|
875
|
+
auto_start_machines = true
|
|
876
|
+
min_machines_running = 0
|
|
877
|
+
processes = ["app"]
|
|
878
|
+
|
|
879
|
+
[[http_service.checks]]
|
|
880
|
+
grace_period = "10s"
|
|
881
|
+
interval = "30s"
|
|
882
|
+
method = "GET"
|
|
883
|
+
timeout = "5s"
|
|
884
|
+
path = "/"
|
|
885
|
+
`;
|
|
886
|
+
if (request.database === "postgres") return shared;
|
|
887
|
+
return `${shared}
|
|
888
|
+
[env]
|
|
889
|
+
DATABASE_URL = "file:/data/sleepy-hollow.db"
|
|
1023
890
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
891
|
+
[mounts]
|
|
892
|
+
source = "data"
|
|
893
|
+
destination = "/data"
|
|
894
|
+
processes = ["app"]
|
|
895
|
+
`;
|
|
1027
896
|
}
|
|
1028
|
-
function
|
|
1029
|
-
|
|
897
|
+
async function read(path) {
|
|
898
|
+
try {
|
|
899
|
+
return await platform.readTextFile(path);
|
|
900
|
+
} catch (error) {
|
|
901
|
+
if (platform.isNotFound(error)) return void 0;
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
1030
904
|
}
|
|
1031
|
-
function
|
|
1032
|
-
return
|
|
905
|
+
function owned(content) {
|
|
906
|
+
return content.startsWith(`${OWNER}
|
|
907
|
+
`);
|
|
1033
908
|
}
|
|
1034
|
-
function
|
|
1035
|
-
|
|
909
|
+
async function artifacts(request, projectRuntime) {
|
|
910
|
+
const desired = [
|
|
911
|
+
{ path: ".dockerignore", content: dockerignore() },
|
|
912
|
+
{ path: "Dockerfile", content: dockerfile(projectRuntime) },
|
|
913
|
+
{ path: "fly.toml", content: flyToml(request) }
|
|
914
|
+
];
|
|
915
|
+
const pending = await Promise.all(desired.map(async (item) => {
|
|
916
|
+
const existing = await read(join(request.projectRoot, item.path));
|
|
917
|
+
if (existing === void 0) return { ...item, status: "created" };
|
|
918
|
+
if (existing === item.content) return { ...item, existing, status: "unchanged" };
|
|
919
|
+
if (!owned(existing)) {
|
|
920
|
+
return failure(
|
|
921
|
+
"SH_DEPLOY_ARTIFACT_UNOWNED",
|
|
922
|
+
`Refusing to replace unrecognized deployment artifact ${item.path}.`,
|
|
923
|
+
"Move the user-owned artifact or create the managed file in a clean project."
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
if (!request.force) {
|
|
927
|
+
return failure(
|
|
928
|
+
"SH_DEPLOY_ARTIFACT_CONFLICT",
|
|
929
|
+
`Managed deployment artifact ${item.path} would change.`,
|
|
930
|
+
"Review the new plan and rerun with --force to replace the managed artifact."
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
return { ...item, existing, status: "updated" };
|
|
934
|
+
}));
|
|
935
|
+
return pending;
|
|
936
|
+
}
|
|
937
|
+
function commands(request) {
|
|
938
|
+
const app = request.target.app;
|
|
939
|
+
if (request.database === "sqlite") {
|
|
940
|
+
return [
|
|
941
|
+
`fly apps create ${app}`,
|
|
942
|
+
`fly volumes create data --app ${app} --region ${request.region}`,
|
|
943
|
+
`fly deploy --app ${app}`
|
|
944
|
+
];
|
|
945
|
+
}
|
|
946
|
+
return [
|
|
947
|
+
`fly apps create ${app}`,
|
|
948
|
+
`fly secrets set DATABASE_URL=<your-postgresql-connection-url> --app ${app}`,
|
|
949
|
+
`fly deploy --app ${app}`
|
|
950
|
+
];
|
|
1036
951
|
}
|
|
1037
|
-
function
|
|
1038
|
-
|
|
952
|
+
async function prepareFlyDeployment(input) {
|
|
953
|
+
const request = normalize(input);
|
|
954
|
+
const projectRuntime = await runtime(request.projectRoot);
|
|
955
|
+
const pending = await artifacts(request, projectRuntime);
|
|
956
|
+
for (const artifact2 of pending) {
|
|
957
|
+
if (artifact2.status !== "unchanged") {
|
|
958
|
+
await platform.writeTextFile(join(request.projectRoot, artifact2.path), artifact2.content);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
schema: "sleepy-hollow-deploy-prepare-result/v1",
|
|
963
|
+
ok: true,
|
|
964
|
+
command: "deploy",
|
|
965
|
+
action: "prepare",
|
|
966
|
+
target: request.target,
|
|
967
|
+
database: request.database,
|
|
968
|
+
...request.region ? { region: request.region } : {},
|
|
969
|
+
artifacts: pending.map((artifact2) => ({
|
|
970
|
+
path: artifact2.path,
|
|
971
|
+
digest: digest(artifact2.content),
|
|
972
|
+
status: artifact2.status
|
|
973
|
+
})),
|
|
974
|
+
environmentKeys: request.database === "postgres" ? ["DATABASE_URL"] : [],
|
|
975
|
+
...request.database === "sqlite" ? { storage: { volume: "data", mountPath: "/data", machines: 1 } } : {},
|
|
976
|
+
commands: commands(request)
|
|
977
|
+
};
|
|
1039
978
|
}
|
|
1040
|
-
function
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
})
|
|
979
|
+
function renderFlyPreparation(result) {
|
|
980
|
+
const lines = [
|
|
981
|
+
`Prepared Fly deployment artifacts for ${result.target.app} (${result.database}).`,
|
|
982
|
+
...result.artifacts.map((artifact2) => ` ${artifact2.status.padEnd(9)} ${artifact2.path} ${artifact2.digest}`),
|
|
983
|
+
...result.storage ? [` volume ${result.storage.volume} \u2192 ${result.storage.mountPath} (${result.storage.machines} writable Machine)`] : [],
|
|
984
|
+
...result.environmentKeys.length ? [` secret ${result.environmentKeys.join(", ")}`] : [],
|
|
985
|
+
"",
|
|
986
|
+
"Next (run these yourself after authenticating with Fly):",
|
|
987
|
+
...result.commands.map((command) => ` ${command}`)
|
|
988
|
+
];
|
|
989
|
+
return `${lines.join("\n")}
|
|
990
|
+
`;
|
|
1045
991
|
}
|
|
1046
992
|
|
|
1047
993
|
// cli/create/create.ts
|
|
1048
|
-
import { join, resolve } from "path";
|
|
994
|
+
import { join as join2, resolve } from "path";
|
|
1049
995
|
|
|
1050
996
|
// cli/create/types.ts
|
|
1051
997
|
var CreationError = class extends Error {
|
|
@@ -1060,7 +1006,7 @@ var CreationError = class extends Error {
|
|
|
1060
1006
|
};
|
|
1061
1007
|
|
|
1062
1008
|
// cli/create/create.ts
|
|
1063
|
-
var VERSION = "0.3.
|
|
1009
|
+
var VERSION = "0.3.2";
|
|
1064
1010
|
var NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1065
1011
|
function files(name) {
|
|
1066
1012
|
return {
|
|
@@ -1105,6 +1051,15 @@ to plan this application. The planning source of truth is
|
|
|
1105
1051
|
\`\`\`bash
|
|
1106
1052
|
npm run verify
|
|
1107
1053
|
\`\`\`
|
|
1054
|
+
|
|
1055
|
+
## Prepare deployment
|
|
1056
|
+
|
|
1057
|
+
After adding a production \`start\` script, prepare reviewable Fly deployment
|
|
1058
|
+
files locally. This command does not log in to or deploy to Fly:
|
|
1059
|
+
|
|
1060
|
+
\`\`\`bash
|
|
1061
|
+
hollow deploy prepare --target fly:${name} --database sqlite --region iad
|
|
1062
|
+
\`\`\`
|
|
1108
1063
|
`,
|
|
1109
1064
|
"tests/capture.ts": `import { rename, writeFile } from "fs/promises";
|
|
1110
1065
|
|
|
@@ -1134,7 +1089,8 @@ test("capture artifact is persisted", async () => { await persist(); });
|
|
|
1134
1089
|
type: "module",
|
|
1135
1090
|
engines: { node: ">=24" },
|
|
1136
1091
|
scripts: { check: "tsc --noEmit", test: "vitest run", verify: "npm run check && npm run test && node .sleepyhollow/verify.ts" },
|
|
1137
|
-
|
|
1092
|
+
dependencies: { "@sleepy-hollow/framework": `^${FRAMEWORK_VERSION}` },
|
|
1093
|
+
devDependencies: { typescript: "5.9.3", vitest: "4.1.11" }
|
|
1138
1094
|
},
|
|
1139
1095
|
null,
|
|
1140
1096
|
2
|
|
@@ -1186,7 +1142,7 @@ async function pathExists(path) {
|
|
|
1186
1142
|
throw error;
|
|
1187
1143
|
}
|
|
1188
1144
|
}
|
|
1189
|
-
var FRAMEWORK_VERSION = "0.3.
|
|
1145
|
+
var FRAMEWORK_VERSION = "0.3.2";
|
|
1190
1146
|
async function createProject(options) {
|
|
1191
1147
|
if (!NAME.test(options.name) || options.name.length > 64) {
|
|
1192
1148
|
throw creationError(
|
|
@@ -1197,7 +1153,7 @@ async function createProject(options) {
|
|
|
1197
1153
|
);
|
|
1198
1154
|
}
|
|
1199
1155
|
const parent = resolve(options.directory);
|
|
1200
|
-
const destination =
|
|
1156
|
+
const destination = join2(parent, options.name);
|
|
1201
1157
|
if (await pathExists(destination)) {
|
|
1202
1158
|
throw creationError(
|
|
1203
1159
|
"SH_CREATE_DESTINATION_EXISTS",
|
|
@@ -1208,14 +1164,14 @@ async function createProject(options) {
|
|
|
1208
1164
|
}
|
|
1209
1165
|
const contents = files(options.name);
|
|
1210
1166
|
const createdFiles = Object.keys(contents).sort();
|
|
1211
|
-
const staging =
|
|
1167
|
+
const staging = join2(
|
|
1212
1168
|
parent,
|
|
1213
1169
|
`.${options.name}.sleepyhollow-${crypto.randomUUID()}`
|
|
1214
1170
|
);
|
|
1215
1171
|
try {
|
|
1216
1172
|
await platform.mkdir(staging);
|
|
1217
1173
|
for (const relative6 of createdFiles) {
|
|
1218
|
-
const target =
|
|
1174
|
+
const target = join2(staging, relative6);
|
|
1219
1175
|
await platform.mkdir(resolve(target, ".."), { recursive: true });
|
|
1220
1176
|
await platform.writeTextFile(target, contents[relative6], { createNew: true });
|
|
1221
1177
|
}
|
|
@@ -1547,8 +1503,8 @@ function normalizeDiagnostics(diagnostics) {
|
|
|
1547
1503
|
)
|
|
1548
1504
|
);
|
|
1549
1505
|
}
|
|
1550
|
-
function renderDevEvent(event,
|
|
1551
|
-
if (
|
|
1506
|
+
function renderDevEvent(event, json2) {
|
|
1507
|
+
if (json2) return JSON.stringify(event);
|
|
1552
1508
|
const label = event.type === "diagnostic" ? `generation ${event.generation} rejected` : event.type === "shutdown" ? `development server stopped (${event.reason ?? "failure"})` : `${event.type} active at ${event.url} (generation ${event.generation}, ${event.routeCount} routes)`;
|
|
1553
1509
|
const lines = [`[${event.sequence}] ${label}`];
|
|
1554
1510
|
if (event.changedFiles?.length) {
|
|
@@ -1575,20 +1531,20 @@ function renderDevEvent(event, json3) {
|
|
|
1575
1531
|
// cli/dev/command.ts
|
|
1576
1532
|
var diagnostic2 = (code, summary, correction) => ({ code, severity: "error", summary, correction });
|
|
1577
1533
|
function parse2(args) {
|
|
1578
|
-
let
|
|
1534
|
+
let json2 = false;
|
|
1579
1535
|
let port = 8e3;
|
|
1580
1536
|
let portSeen = false;
|
|
1581
1537
|
for (let index = 0; index < args.length; index += 1) {
|
|
1582
1538
|
const value = args[index];
|
|
1583
1539
|
if (value === "--json") {
|
|
1584
|
-
if (
|
|
1540
|
+
if (json2) {
|
|
1585
1541
|
return diagnostic2(
|
|
1586
1542
|
"SH_DEV_USAGE_INVALID",
|
|
1587
1543
|
"The --json option was provided more than once",
|
|
1588
1544
|
"Use hollow dev [--port <1-65535>] [--json]."
|
|
1589
1545
|
);
|
|
1590
1546
|
}
|
|
1591
|
-
|
|
1547
|
+
json2 = true;
|
|
1592
1548
|
continue;
|
|
1593
1549
|
}
|
|
1594
1550
|
if (value === "--port") {
|
|
@@ -1624,9 +1580,9 @@ function parse2(args) {
|
|
|
1624
1580
|
"Use hollow dev [--port <1-65535>] [--json]."
|
|
1625
1581
|
);
|
|
1626
1582
|
}
|
|
1627
|
-
return { json:
|
|
1583
|
+
return { json: json2, port };
|
|
1628
1584
|
}
|
|
1629
|
-
function
|
|
1585
|
+
function failure2(error, fallback) {
|
|
1630
1586
|
return normalizeDiagnostics(
|
|
1631
1587
|
error instanceof DevCommandError ? error.diagnostics : [fallback]
|
|
1632
1588
|
);
|
|
@@ -1733,7 +1689,7 @@ ${parsed.correction}`
|
|
|
1733
1689
|
diagnostics: []
|
|
1734
1690
|
});
|
|
1735
1691
|
} catch (error) {
|
|
1736
|
-
const diagnostics =
|
|
1692
|
+
const diagnostics = failure2(
|
|
1737
1693
|
error,
|
|
1738
1694
|
diagnostic2(
|
|
1739
1695
|
"SH_DEV_STARTUP_FAILED",
|
|
@@ -1759,7 +1715,7 @@ ${parsed.correction}`
|
|
|
1759
1715
|
type: "diagnostic",
|
|
1760
1716
|
state: "rejected",
|
|
1761
1717
|
generation,
|
|
1762
|
-
diagnostics:
|
|
1718
|
+
diagnostics: failure2(
|
|
1763
1719
|
error,
|
|
1764
1720
|
diagnostic2(
|
|
1765
1721
|
"SH_DEV_WATCH_FAILED",
|
|
@@ -1808,7 +1764,7 @@ ${parsed.correction}`
|
|
|
1808
1764
|
state: "rejected",
|
|
1809
1765
|
generation: nextGeneration,
|
|
1810
1766
|
changedFiles: changes,
|
|
1811
|
-
diagnostics:
|
|
1767
|
+
diagnostics: failure2(
|
|
1812
1768
|
error,
|
|
1813
1769
|
diagnostic2(
|
|
1814
1770
|
"SH_DEV_RELOAD_FAILED",
|
|
@@ -1843,7 +1799,7 @@ ${parsed.correction}`
|
|
|
1843
1799
|
state: "rejected",
|
|
1844
1800
|
generation: nextGeneration,
|
|
1845
1801
|
changedFiles: changes,
|
|
1846
|
-
diagnostics:
|
|
1802
|
+
diagnostics: failure2(
|
|
1847
1803
|
error,
|
|
1848
1804
|
diagnostic2(
|
|
1849
1805
|
"SH_DEV_ACTIVATION_FAILED",
|
|
@@ -1876,7 +1832,7 @@ ${parsed.correction}`
|
|
|
1876
1832
|
type: "diagnostic",
|
|
1877
1833
|
state: "rejected",
|
|
1878
1834
|
generation,
|
|
1879
|
-
diagnostics:
|
|
1835
|
+
diagnostics: failure2(
|
|
1880
1836
|
error,
|
|
1881
1837
|
diagnostic2(
|
|
1882
1838
|
"SH_DEV_SUPERVISOR_FAILED",
|
|
@@ -1999,12 +1955,12 @@ async function loadRuntime(projectRoot) {
|
|
|
1999
1955
|
});
|
|
2000
1956
|
}
|
|
2001
1957
|
const routes2 = await discoverRoutes(realApiRoot);
|
|
2002
|
-
const
|
|
1958
|
+
const runtime2 = await composeProjectSecurity(routes2, {
|
|
2003
1959
|
mode: "development",
|
|
2004
1960
|
root,
|
|
2005
1961
|
...project.securityModule === void 0 ? {} : { securityModule: project.securityModule }
|
|
2006
1962
|
});
|
|
2007
|
-
return { runtime, routeCount:
|
|
1963
|
+
return { runtime: runtime2, routeCount: runtime2.routes.length };
|
|
2008
1964
|
}
|
|
2009
1965
|
async function runDevWorker(args) {
|
|
2010
1966
|
if (args.length !== 4 || !["validate", "serve"].includes(args[0])) return 2;
|
|
@@ -2012,12 +1968,12 @@ async function runDevWorker(args) {
|
|
|
2012
1968
|
const port = Number(rawPort);
|
|
2013
1969
|
if (hostname !== "127.0.0.1" || !Number.isInteger(port) || port < 1 || port > 65535) return 2;
|
|
2014
1970
|
try {
|
|
2015
|
-
const { runtime, routeCount } = await loadRuntime(projectRoot);
|
|
1971
|
+
const { runtime: runtime2, routeCount } = await loadRuntime(projectRoot);
|
|
2016
1972
|
if (intent === "validate") {
|
|
2017
1973
|
console.log(JSON.stringify({ ready: true, routeCount }));
|
|
2018
1974
|
return 0;
|
|
2019
1975
|
}
|
|
2020
|
-
const server = platform.serve({ hostname, port }, (request) =>
|
|
1976
|
+
const server = platform.serve({ hostname, port }, (request) => runtime2.fetch(request));
|
|
2021
1977
|
console.log(JSON.stringify({ ready: true, routeCount }));
|
|
2022
1978
|
await new Promise((resolve5, reject) => {
|
|
2023
1979
|
server.once("close", resolve5);
|
|
@@ -2036,10 +1992,10 @@ async function runDevWorker(args) {
|
|
|
2036
1992
|
}
|
|
2037
1993
|
|
|
2038
1994
|
// cli/generate/artifacts.ts
|
|
2039
|
-
import { basename, dirname, join as
|
|
1995
|
+
import { basename, dirname, join as join3 } from "path";
|
|
2040
1996
|
|
|
2041
1997
|
// cli/generate/canonical.ts
|
|
2042
|
-
import { createHash } from "crypto";
|
|
1998
|
+
import { createHash as createHash2 } from "crypto";
|
|
2043
1999
|
function stable(value) {
|
|
2044
2000
|
if (Array.isArray(value)) return value.map(stable);
|
|
2045
2001
|
if (value && typeof value === "object") {
|
|
@@ -2053,8 +2009,8 @@ function canonicalJson(value) {
|
|
|
2053
2009
|
return `${JSON.stringify(stable(value), null, 2)}
|
|
2054
2010
|
`;
|
|
2055
2011
|
}
|
|
2056
|
-
function
|
|
2057
|
-
return
|
|
2012
|
+
function digest2(content) {
|
|
2013
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
2058
2014
|
}
|
|
2059
2015
|
function stableSummary(value) {
|
|
2060
2016
|
return JSON.stringify(stable(value));
|
|
@@ -2909,7 +2865,7 @@ var ownedPaths = [
|
|
|
2909
2865
|
"manifest.json"
|
|
2910
2866
|
];
|
|
2911
2867
|
function artifact(path, content) {
|
|
2912
|
-
return { path, content, digest:
|
|
2868
|
+
return { path, content, digest: digest2(content) };
|
|
2913
2869
|
}
|
|
2914
2870
|
function renderArtifacts(inventory2) {
|
|
2915
2871
|
const normalized = normalizeInventory(inventory2);
|
|
@@ -2924,15 +2880,15 @@ function renderArtifacts(inventory2) {
|
|
|
2924
2880
|
];
|
|
2925
2881
|
const manifestContent = canonicalJson({
|
|
2926
2882
|
schema: "sleepy-hollow-generated-manifest/v1",
|
|
2927
|
-
generatorVersion: "0.3.
|
|
2883
|
+
generatorVersion: "0.3.2",
|
|
2928
2884
|
serviceId: normalized.serviceId,
|
|
2929
|
-
inputDigest:
|
|
2885
|
+
inputDigest: digest2(input),
|
|
2930
2886
|
artifacts: Object.fromEntries(
|
|
2931
2887
|
rendered.map((item) => [item.path, item.digest])
|
|
2932
2888
|
)
|
|
2933
2889
|
});
|
|
2934
2890
|
return {
|
|
2935
|
-
inputDigest:
|
|
2891
|
+
inputDigest: digest2(input),
|
|
2936
2892
|
artifacts: [...rendered, artifact("manifest.json", manifestContent)]
|
|
2937
2893
|
};
|
|
2938
2894
|
}
|
|
@@ -2958,7 +2914,7 @@ async function copyEntry(source, target) {
|
|
|
2958
2914
|
if (info.isDirectory()) {
|
|
2959
2915
|
await platform.mkdir(target, { recursive: true });
|
|
2960
2916
|
for await (const entry of platform.readDir(source)) {
|
|
2961
|
-
await copyEntry(
|
|
2917
|
+
await copyEntry(join3(source, entry.name), join3(target, entry.name));
|
|
2962
2918
|
}
|
|
2963
2919
|
return;
|
|
2964
2920
|
}
|
|
@@ -2975,28 +2931,28 @@ function outputFailure(error) {
|
|
|
2975
2931
|
}]);
|
|
2976
2932
|
}
|
|
2977
2933
|
async function previousOwned(generatedDirectory) {
|
|
2978
|
-
const source = await readText(
|
|
2934
|
+
const source = await readText(join3(generatedDirectory, "manifest.json"));
|
|
2979
2935
|
if (!source) return void 0;
|
|
2980
2936
|
try {
|
|
2981
2937
|
const manifest = JSON.parse(source);
|
|
2982
2938
|
if (manifest.schema !== "sleepy-hollow-generated-manifest/v1") {
|
|
2983
2939
|
return void 0;
|
|
2984
2940
|
}
|
|
2985
|
-
const
|
|
2986
|
-
if (!
|
|
2987
|
-
return /* @__PURE__ */ new Set([...Object.keys(
|
|
2941
|
+
const artifacts2 = manifest.artifacts;
|
|
2942
|
+
if (!artifacts2 || typeof artifacts2 !== "object" || Array.isArray(artifacts2)) return void 0;
|
|
2943
|
+
return /* @__PURE__ */ new Set([...Object.keys(artifacts2), "manifest.json"]);
|
|
2988
2944
|
} catch {
|
|
2989
2945
|
return void 0;
|
|
2990
2946
|
}
|
|
2991
2947
|
}
|
|
2992
2948
|
async function writeAtomically(projectRoot, rendered) {
|
|
2993
|
-
const target =
|
|
2949
|
+
const target = join3(projectRoot, "generated");
|
|
2994
2950
|
await platform.mkdir(projectRoot, { recursive: true });
|
|
2995
2951
|
const targetExists = await exists(target);
|
|
2996
2952
|
const priorOwned = targetExists ? await previousOwned(target) : void 0;
|
|
2997
2953
|
if (targetExists && !priorOwned) {
|
|
2998
2954
|
for (const path of ownedPaths) {
|
|
2999
|
-
if (await exists(
|
|
2955
|
+
if (await exists(join3(target, path))) {
|
|
3000
2956
|
throw new GenerationError([{
|
|
3001
2957
|
code: "SH_GENERATE_UNKNOWN_TARGET",
|
|
3002
2958
|
summary: `Refusing to replace unowned file generated/${path}`,
|
|
@@ -3016,11 +2972,11 @@ async function writeAtomically(projectRoot, rendered) {
|
|
|
3016
2972
|
if (targetExists) {
|
|
3017
2973
|
for await (const entry of platform.readDir(target)) {
|
|
3018
2974
|
if (priorOwned?.has(entry.name)) continue;
|
|
3019
|
-
await copyEntry(
|
|
2975
|
+
await copyEntry(join3(target, entry.name), join3(stage, entry.name));
|
|
3020
2976
|
}
|
|
3021
2977
|
}
|
|
3022
2978
|
for (const item of rendered.artifacts) {
|
|
3023
|
-
await platform.writeTextFile(
|
|
2979
|
+
await platform.writeTextFile(join3(stage, item.path), item.content);
|
|
3024
2980
|
}
|
|
3025
2981
|
if (targetExists) {
|
|
3026
2982
|
await platform.rename(target, backup);
|
|
@@ -3038,10 +2994,10 @@ async function writeAtomically(projectRoot, rendered) {
|
|
|
3038
2994
|
}
|
|
3039
2995
|
async function generate(options) {
|
|
3040
2996
|
const rendered = renderArtifacts(options.inventory);
|
|
3041
|
-
const generatedDirectory =
|
|
3042
|
-
const
|
|
3043
|
-
const current = await readText(
|
|
3044
|
-
const actualDigest = current === void 0 ? void 0 :
|
|
2997
|
+
const generatedDirectory = join3(options.projectRoot, "generated");
|
|
2998
|
+
const artifacts2 = await Promise.all(rendered.artifacts.map(async (item) => {
|
|
2999
|
+
const current = await readText(join3(generatedDirectory, item.path));
|
|
3000
|
+
const actualDigest = current === void 0 ? void 0 : digest2(current);
|
|
3045
3001
|
return {
|
|
3046
3002
|
path: `generated/${item.path}`,
|
|
3047
3003
|
digest: item.digest,
|
|
@@ -3054,7 +3010,7 @@ async function generate(options) {
|
|
|
3054
3010
|
);
|
|
3055
3011
|
let previous = options.previousOpenApi;
|
|
3056
3012
|
if (!previous) {
|
|
3057
|
-
const source = await readText(
|
|
3013
|
+
const source = await readText(join3(generatedDirectory, "openapi.json"));
|
|
3058
3014
|
if (source) {
|
|
3059
3015
|
try {
|
|
3060
3016
|
previous = JSON.parse(source);
|
|
@@ -3072,7 +3028,7 @@ async function generate(options) {
|
|
|
3072
3028
|
digest: "absent",
|
|
3073
3029
|
stale: true
|
|
3074
3030
|
}));
|
|
3075
|
-
const inspectedArtifacts = [...
|
|
3031
|
+
const inspectedArtifacts = [...artifacts2, ...extraOwned];
|
|
3076
3032
|
const stale = inspectedArtifacts.filter((item) => item.stale);
|
|
3077
3033
|
const diagnostics = stale.map((item) => ({
|
|
3078
3034
|
code: "SH_GENERATE_ARTIFACT_STALE",
|
|
@@ -3100,7 +3056,7 @@ async function generate(options) {
|
|
|
3100
3056
|
schema: "sleepy-hollow-generate-result/v1",
|
|
3101
3057
|
serviceId: options.inventory.serviceId,
|
|
3102
3058
|
inputDigest: rendered.inputDigest,
|
|
3103
|
-
artifacts:
|
|
3059
|
+
artifacts: artifacts2.map((item) => ({
|
|
3104
3060
|
...item,
|
|
3105
3061
|
actualDigest: item.digest,
|
|
3106
3062
|
stale: false
|
|
@@ -3198,7 +3154,7 @@ function inventoryFromRoutes(routes2, options) {
|
|
|
3198
3154
|
return {
|
|
3199
3155
|
serviceId: options.serviceId,
|
|
3200
3156
|
title: options.title ?? options.serviceId,
|
|
3201
|
-
version: options.version ?? "0.3.
|
|
3157
|
+
version: options.version ?? "0.3.2",
|
|
3202
3158
|
...options.description ? { description: options.description } : {},
|
|
3203
3159
|
operations,
|
|
3204
3160
|
securitySchemes: options.securitySchemes ?? {}
|
|
@@ -3211,7 +3167,7 @@ function generateContracts(options) {
|
|
|
3211
3167
|
}
|
|
3212
3168
|
|
|
3213
3169
|
// cli/test/result.ts
|
|
3214
|
-
import { createHash as
|
|
3170
|
+
import { createHash as createHash3 } from "crypto";
|
|
3215
3171
|
var MAX_EVIDENCE = 8 * 1024;
|
|
3216
3172
|
function safe(value) {
|
|
3217
3173
|
return value.replace(
|
|
@@ -3235,12 +3191,12 @@ function eventKey(event) {
|
|
|
3235
3191
|
return `${event.file}\0${event.name}`;
|
|
3236
3192
|
}
|
|
3237
3193
|
function unmappedId(event) {
|
|
3238
|
-
return `unmapped:${
|
|
3194
|
+
return `unmapped:${createHash3("sha256").update(eventKey(event)).digest("hex").slice(0, 12)}`;
|
|
3239
3195
|
}
|
|
3240
|
-
function normalizeRunnerResult(
|
|
3241
|
-
const diagnostics = [...
|
|
3196
|
+
function normalizeRunnerResult(plan2, inventory2, runner) {
|
|
3197
|
+
const diagnostics = [...plan2.diagnostics];
|
|
3242
3198
|
const selectedEntries = inventory2.manifest.tests.filter(
|
|
3243
|
-
(test) =>
|
|
3199
|
+
(test) => plan2.selectedTests.includes(test.id)
|
|
3244
3200
|
);
|
|
3245
3201
|
const byEvent = new Map(selectedEntries.map((test) => [
|
|
3246
3202
|
eventKey({ file: test.sourcePath, name: test.registeredName }),
|
|
@@ -3343,7 +3299,7 @@ function normalizeRunnerResult(plan3, inventory2, runner) {
|
|
|
3343
3299
|
}
|
|
3344
3300
|
tests.sort((left, right) => left.id.localeCompare(right.id));
|
|
3345
3301
|
const selectedRequirements = inventory2.requirements.filter(
|
|
3346
|
-
(requirement) =>
|
|
3302
|
+
(requirement) => plan2.selectedRequirements.includes(requirement.id)
|
|
3347
3303
|
);
|
|
3348
3304
|
const selectedManifest = {
|
|
3349
3305
|
schema: "sleepy-hollow-test-manifest/v1",
|
|
@@ -3373,10 +3329,10 @@ function normalizeRunnerResult(plan3, inventory2, runner) {
|
|
|
3373
3329
|
schema: "sleepy-hollow-test-result/v1",
|
|
3374
3330
|
ok,
|
|
3375
3331
|
command: "test",
|
|
3376
|
-
requestedScope:
|
|
3377
|
-
effectiveScope:
|
|
3378
|
-
selectedRequirements: Object.freeze([...
|
|
3379
|
-
selectedTests: Object.freeze([...
|
|
3332
|
+
requestedScope: plan2.requestedScope,
|
|
3333
|
+
effectiveScope: plan2.effectiveScope,
|
|
3334
|
+
selectedRequirements: Object.freeze([...plan2.selectedRequirements]),
|
|
3335
|
+
selectedTests: Object.freeze([...plan2.selectedTests]),
|
|
3380
3336
|
tests: Object.freeze(tests),
|
|
3381
3337
|
criteria: Object.freeze(criteria),
|
|
3382
3338
|
diagnostics: Object.freeze(diagnostics),
|
|
@@ -3394,8 +3350,8 @@ function normalizeRunnerResult(plan3, inventory2, runner) {
|
|
|
3394
3350
|
verificationStateChanged: false
|
|
3395
3351
|
});
|
|
3396
3352
|
}
|
|
3397
|
-
function failedWithoutRunner(
|
|
3398
|
-
return normalizeRunnerResult(
|
|
3353
|
+
function failedWithoutRunner(plan2, inventory2) {
|
|
3354
|
+
return normalizeRunnerResult(plan2, inventory2, {
|
|
3399
3355
|
status: "failed",
|
|
3400
3356
|
durationMs: 0,
|
|
3401
3357
|
events: []
|
|
@@ -3418,9 +3374,9 @@ function projectPath(root, path) {
|
|
|
3418
3374
|
}
|
|
3419
3375
|
return local;
|
|
3420
3376
|
}
|
|
3421
|
-
function invocation(
|
|
3377
|
+
function invocation(plan2, options) {
|
|
3422
3378
|
const root = resolve4(options.projectRoot);
|
|
3423
|
-
const files2 = [...new Set(
|
|
3379
|
+
const files2 = [...new Set(plan2.files.map((path) => projectPath(root, path)))].sort();
|
|
3424
3380
|
if (files2.length === 0) {
|
|
3425
3381
|
throw new TypeError(
|
|
3426
3382
|
"A native test run requires at least one exact source file"
|
|
@@ -3430,7 +3386,7 @@ function invocation(plan3, options) {
|
|
|
3430
3386
|
"./node_modules/vitest/vitest.mjs",
|
|
3431
3387
|
"run",
|
|
3432
3388
|
"--reporter=verbose",
|
|
3433
|
-
...
|
|
3389
|
+
...plan2.filter ? ["--testNamePattern", plan2.filter] : [],
|
|
3434
3390
|
...files2
|
|
3435
3391
|
];
|
|
3436
3392
|
return Object.freeze({
|
|
@@ -3489,14 +3445,14 @@ function parseTap(source, inventory2, failureEvidence) {
|
|
|
3489
3445
|
}
|
|
3490
3446
|
return { valid: validHeader && validPlan && validStructure, events };
|
|
3491
3447
|
}
|
|
3492
|
-
async function runNative(
|
|
3448
|
+
async function runNative(plan2, inventory2, options) {
|
|
3493
3449
|
const timeoutMs = options.timeoutMs ?? MAX_TIMEOUT2;
|
|
3494
3450
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMEOUT2) {
|
|
3495
3451
|
throw new TypeError(
|
|
3496
3452
|
"Test timeout must be from 1 through 600000 milliseconds"
|
|
3497
3453
|
);
|
|
3498
3454
|
}
|
|
3499
|
-
const command = invocation(
|
|
3455
|
+
const command = invocation(plan2, options);
|
|
3500
3456
|
const started = performance.now();
|
|
3501
3457
|
const child = new platform.Command(command.command, {
|
|
3502
3458
|
cwd: command.cwd,
|
|
@@ -3556,7 +3512,7 @@ async function runNative(plan3, inventory2, options) {
|
|
|
3556
3512
|
}
|
|
3557
3513
|
|
|
3558
3514
|
// cli/test/scope.ts
|
|
3559
|
-
import { isAbsolute as isAbsolute4, normalize, sep as sep5 } from "path";
|
|
3515
|
+
import { isAbsolute as isAbsolute4, normalize as normalize2, sep as sep5 } from "path";
|
|
3560
3516
|
var stableId = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
3561
3517
|
function sorted2(values) {
|
|
3562
3518
|
return [...new Set(values)].sort();
|
|
@@ -3572,13 +3528,13 @@ function diagnostic4(code, severity, summary, correction, requirementId) {
|
|
|
3572
3528
|
}
|
|
3573
3529
|
function safePath(path) {
|
|
3574
3530
|
const portable3 = path.split(sep5).join("/");
|
|
3575
|
-
const normalized =
|
|
3531
|
+
const normalized = normalize2(path).split(sep5).join("/");
|
|
3576
3532
|
return !isAbsolute4(path) && portable3 !== "" && portable3 !== ".." && !portable3.startsWith("../") && normalized === portable3 && !portable3.includes("\0");
|
|
3577
3533
|
}
|
|
3578
3534
|
function escaped(value) {
|
|
3579
3535
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3580
3536
|
}
|
|
3581
|
-
function
|
|
3537
|
+
function plan(inventory2, requestedScope) {
|
|
3582
3538
|
const diagnostics = [];
|
|
3583
3539
|
const requirements2 = /* @__PURE__ */ new Map();
|
|
3584
3540
|
for (const requirement of inventory2.requirements) {
|
|
@@ -3843,14 +3799,14 @@ function renderJson(result) {
|
|
|
3843
3799
|
|
|
3844
3800
|
// cli/test/command.ts
|
|
3845
3801
|
function parse3(args) {
|
|
3846
|
-
let
|
|
3802
|
+
let json2 = false;
|
|
3847
3803
|
let scope = { kind: "full" };
|
|
3848
3804
|
let scopeSeen = false;
|
|
3849
3805
|
for (let index = 0; index < args.length; index++) {
|
|
3850
3806
|
const argument = args[index];
|
|
3851
3807
|
if (argument === "--json") {
|
|
3852
|
-
if (
|
|
3853
|
-
|
|
3808
|
+
if (json2) throw new TypeError("--json may appear once");
|
|
3809
|
+
json2 = true;
|
|
3854
3810
|
continue;
|
|
3855
3811
|
}
|
|
3856
3812
|
if (argument === "--full") {
|
|
@@ -3893,9 +3849,9 @@ function parse3(args) {
|
|
|
3893
3849
|
}
|
|
3894
3850
|
throw new TypeError(`Unknown test argument: ${argument.slice(0, 80)}`);
|
|
3895
3851
|
}
|
|
3896
|
-
return { json:
|
|
3852
|
+
return { json: json2, scope };
|
|
3897
3853
|
}
|
|
3898
|
-
function usage(summary,
|
|
3854
|
+
function usage(summary, json2, io) {
|
|
3899
3855
|
const result = {
|
|
3900
3856
|
ok: false,
|
|
3901
3857
|
command: "test",
|
|
@@ -3908,7 +3864,7 @@ function usage(summary, json3, io) {
|
|
|
3908
3864
|
}]
|
|
3909
3865
|
};
|
|
3910
3866
|
io.stderr(
|
|
3911
|
-
|
|
3867
|
+
json2 ? JSON.stringify(result) : `SH_TEST_USAGE_INVALID: ${summary}
|
|
3912
3868
|
${result.diagnostics[0].correction}`
|
|
3913
3869
|
);
|
|
3914
3870
|
return 2;
|
|
@@ -3944,9 +3900,9 @@ function loadFailure(scope) {
|
|
|
3944
3900
|
verificationStateChanged: false
|
|
3945
3901
|
};
|
|
3946
3902
|
}
|
|
3947
|
-
function emit(result,
|
|
3903
|
+
function emit(result, json2, io) {
|
|
3948
3904
|
(result.ok ? io.stdout : io.stderr)(
|
|
3949
|
-
|
|
3905
|
+
json2 ? renderJson(result) : renderHuman(result)
|
|
3950
3906
|
);
|
|
3951
3907
|
return result.ok ? 0 : 1;
|
|
3952
3908
|
}
|
|
@@ -3969,7 +3925,7 @@ async function execute(args, io, load, runner) {
|
|
|
3969
3925
|
}
|
|
3970
3926
|
const capturePath = `${io.cwd}/generated/capture.json`;
|
|
3971
3927
|
inventory2 = { ...inventory2, captureArtifactPath: capturePath };
|
|
3972
|
-
const testPlan =
|
|
3928
|
+
const testPlan = plan(inventory2, parsed.scope);
|
|
3973
3929
|
if (testPlan.diagnostics.some((item) => item.severity === "error")) {
|
|
3974
3930
|
return emit(failedWithoutRunner(testPlan, inventory2), parsed.json, io);
|
|
3975
3931
|
}
|
|
@@ -4052,7 +4008,7 @@ var CLI_COMMANDS = [
|
|
|
4052
4008
|
"generate",
|
|
4053
4009
|
"deploy"
|
|
4054
4010
|
];
|
|
4055
|
-
var CLI_VERSION = "0.3.
|
|
4011
|
+
var CLI_VERSION = "0.3.2";
|
|
4056
4012
|
var commandMetadata = {
|
|
4057
4013
|
create: {
|
|
4058
4014
|
description: "Create one deterministic Sleepy Hollow project.",
|
|
@@ -4075,8 +4031,8 @@ var commandMetadata = {
|
|
|
4075
4031
|
usage: "hollow generate [--check] [--json]"
|
|
4076
4032
|
},
|
|
4077
4033
|
deploy: {
|
|
4078
|
-
description: "
|
|
4079
|
-
usage: "hollow deploy
|
|
4034
|
+
description: "Prepare local Fly deployment artifacts; Fly performs deployment.",
|
|
4035
|
+
usage: "hollow deploy prepare --target fly:<app> --database <sqlite|postgres> [--region <region>] [--force] [--json]"
|
|
4080
4036
|
}
|
|
4081
4037
|
};
|
|
4082
4038
|
function topLevelHelp() {
|
|
@@ -4207,9 +4163,9 @@ function renderJson2(result) {
|
|
|
4207
4163
|
diagnostics: result.diagnostics
|
|
4208
4164
|
});
|
|
4209
4165
|
}
|
|
4210
|
-
function emitResult(result,
|
|
4166
|
+
function emitResult(result, json2, io, exitCode) {
|
|
4211
4167
|
const normalized = normalizeResult(result);
|
|
4212
|
-
const output =
|
|
4168
|
+
const output = json2 ? renderJson2(normalized) : renderHuman2(normalized);
|
|
4213
4169
|
(normalized.ok ? io.stdout : io.stderr)(output);
|
|
4214
4170
|
return exitCode ?? (normalized.ok ? 0 : 1);
|
|
4215
4171
|
}
|
|
@@ -4227,9 +4183,9 @@ function usageResult(code, summary) {
|
|
|
4227
4183
|
}]
|
|
4228
4184
|
});
|
|
4229
4185
|
}
|
|
4230
|
-
function emitUsage(code, summary,
|
|
4186
|
+
function emitUsage(code, summary, json2, io) {
|
|
4231
4187
|
io.stderr(
|
|
4232
|
-
|
|
4188
|
+
json2 ? usageResult(code, summary) : `${code}: ${summary}
|
|
4233
4189
|
Run hollow --help and use one documented invocation.`
|
|
4234
4190
|
);
|
|
4235
4191
|
return 2;
|
|
@@ -4264,12 +4220,12 @@ function confirmationFailure(result) {
|
|
|
4264
4220
|
};
|
|
4265
4221
|
}
|
|
4266
4222
|
async function runCommandSurface(args, io, handlers) {
|
|
4267
|
-
const
|
|
4223
|
+
const json2 = args.includes("--json");
|
|
4268
4224
|
if (containsModelOption(args)) {
|
|
4269
4225
|
return emitUsage(
|
|
4270
4226
|
"SH_CLI_USAGE_INVALID",
|
|
4271
4227
|
"Model selection is not part of the Sleepy Hollow CLI.",
|
|
4272
|
-
|
|
4228
|
+
json2,
|
|
4273
4229
|
io
|
|
4274
4230
|
);
|
|
4275
4231
|
}
|
|
@@ -4289,7 +4245,7 @@ async function runCommandSurface(args, io, handlers) {
|
|
|
4289
4245
|
return emitUsage(
|
|
4290
4246
|
"SH_CLI_USAGE_INVALID",
|
|
4291
4247
|
"Help expects exactly one supported command.",
|
|
4292
|
-
|
|
4248
|
+
json2,
|
|
4293
4249
|
io
|
|
4294
4250
|
);
|
|
4295
4251
|
}
|
|
@@ -4298,7 +4254,7 @@ async function runCommandSurface(args, io, handlers) {
|
|
|
4298
4254
|
return emitUsage(
|
|
4299
4255
|
invalid === "option" ? "SH_CLI_USAGE_INVALID" : "SH_CLI_COMMAND_UNKNOWN",
|
|
4300
4256
|
`Unknown ${invalid}: ${safeToken(args[0])}`,
|
|
4301
|
-
|
|
4257
|
+
json2,
|
|
4302
4258
|
io
|
|
4303
4259
|
);
|
|
4304
4260
|
}
|
|
@@ -4311,7 +4267,7 @@ async function runCommandSurface(args, io, handlers) {
|
|
|
4311
4267
|
return emitUsage(
|
|
4312
4268
|
"SH_CLI_USAGE_INVALID",
|
|
4313
4269
|
`Invalid help invocation for ${selected}.`,
|
|
4314
|
-
|
|
4270
|
+
json2,
|
|
4315
4271
|
io
|
|
4316
4272
|
);
|
|
4317
4273
|
}
|
|
@@ -4319,7 +4275,7 @@ async function runCommandSurface(args, io, handlers) {
|
|
|
4319
4275
|
const response = await handlers[selected]({
|
|
4320
4276
|
args: args.slice(1),
|
|
4321
4277
|
cwd: io.cwd,
|
|
4322
|
-
json:
|
|
4278
|
+
json: json2,
|
|
4323
4279
|
io
|
|
4324
4280
|
});
|
|
4325
4281
|
if (response.result.command !== selected) {
|
|
@@ -4337,14 +4293,14 @@ async function runCommandSurface(args, io, handlers) {
|
|
|
4337
4293
|
}
|
|
4338
4294
|
if (!response.result.ok) {
|
|
4339
4295
|
if (response.rendered) return response.exitCode ?? 1;
|
|
4340
|
-
return emitResult(response.result,
|
|
4296
|
+
return emitResult(response.result, json2, io, response.exitCode);
|
|
4341
4297
|
}
|
|
4342
4298
|
if (response.rendered) return response.exitCode ?? 0;
|
|
4343
4299
|
if (!response.operation || response.operation.intent === "preview") {
|
|
4344
|
-
return emitResult(response.result,
|
|
4300
|
+
return emitResult(response.result, json2, io, response.exitCode);
|
|
4345
4301
|
}
|
|
4346
4302
|
if (!response.operation.providedConfirmation || response.operation.providedConfirmation !== response.operation.confirmationDigest) {
|
|
4347
|
-
return emitResult(confirmationFailure(response.result),
|
|
4303
|
+
return emitResult(confirmationFailure(response.result), json2, io);
|
|
4348
4304
|
}
|
|
4349
4305
|
const applied = await response.operation.apply();
|
|
4350
4306
|
if (applied.command !== selected) {
|
|
@@ -4353,7 +4309,7 @@ async function runCommandSurface(args, io, handlers) {
|
|
|
4353
4309
|
if (!applied.schema && !applied.version) {
|
|
4354
4310
|
throw new TypeError("Command application returned an unversioned result");
|
|
4355
4311
|
}
|
|
4356
|
-
return emitResult(applied,
|
|
4312
|
+
return emitResult(applied, json2, io);
|
|
4357
4313
|
} catch {
|
|
4358
4314
|
return emitResult(
|
|
4359
4315
|
{
|
|
@@ -4368,7 +4324,7 @@ async function runCommandSurface(args, io, handlers) {
|
|
|
4368
4324
|
correction: "Inspect the project evidence and retry the documented command."
|
|
4369
4325
|
}]
|
|
4370
4326
|
},
|
|
4371
|
-
|
|
4327
|
+
json2,
|
|
4372
4328
|
io
|
|
4373
4329
|
);
|
|
4374
4330
|
}
|
|
@@ -4438,7 +4394,7 @@ ${created.nextActions.map((item) => ` ${item}`).join("\n")}`,
|
|
|
4438
4394
|
summary: "Project creation failed",
|
|
4439
4395
|
correction: "Inspect the protected failure and retry safely."
|
|
4440
4396
|
}];
|
|
4441
|
-
const
|
|
4397
|
+
const json2 = {
|
|
4442
4398
|
ok: false,
|
|
4443
4399
|
command: "create",
|
|
4444
4400
|
version: CLI_VERSION,
|
|
@@ -4454,7 +4410,7 @@ ${created.nextActions.map((item) => ` ${item}`).join("\n")}`,
|
|
|
4454
4410
|
version: CLI_VERSION,
|
|
4455
4411
|
summary: "Project creation failed.",
|
|
4456
4412
|
diagnostics: creationDiagnostics(diagnostics, cwd),
|
|
4457
|
-
json:
|
|
4413
|
+
json: json2
|
|
4458
4414
|
}
|
|
4459
4415
|
};
|
|
4460
4416
|
}
|
|
@@ -4500,11 +4456,11 @@ var generate2 = async ({ args, cwd }) => {
|
|
|
4500
4456
|
}
|
|
4501
4457
|
const check = args.includes("--check");
|
|
4502
4458
|
try {
|
|
4503
|
-
const configPath =
|
|
4459
|
+
const configPath = join4(cwd, "sleepyhollow.config.ts");
|
|
4504
4460
|
const imported = await import(`${pathToFileURL2(configPath).href}?generate=${Date.now()}`);
|
|
4505
4461
|
const config = imported.default;
|
|
4506
4462
|
const routes2 = await discoverRoutes(
|
|
4507
|
-
|
|
4463
|
+
join4(cwd, config.apiDirectory ?? "api")
|
|
4508
4464
|
);
|
|
4509
4465
|
const generated = await generateContracts({
|
|
4510
4466
|
inventory: inventoryFromRoutes(routes2, {
|
|
@@ -4538,7 +4494,7 @@ Review ${generated.changes.length} contract change(s).` : ""}`;
|
|
|
4538
4494
|
summary: "Contract generation failed",
|
|
4539
4495
|
correction: "Inspect route, schema, configuration, and output diagnostics before retrying."
|
|
4540
4496
|
}];
|
|
4541
|
-
const
|
|
4497
|
+
const json2 = {
|
|
4542
4498
|
ok: false,
|
|
4543
4499
|
command: "generate",
|
|
4544
4500
|
schema: "sleepy-hollow-generate-result/v1",
|
|
@@ -4556,7 +4512,7 @@ Review ${generated.changes.length} contract change(s).` : ""}`;
|
|
|
4556
4512
|
schema: "sleepy-hollow-generate-result/v1",
|
|
4557
4513
|
summary: "Contract generation failed.",
|
|
4558
4514
|
diagnostics: generationDiagnostics(diagnostics),
|
|
4559
|
-
json:
|
|
4515
|
+
json: json2
|
|
4560
4516
|
}
|
|
4561
4517
|
};
|
|
4562
4518
|
}
|
|
@@ -4618,123 +4574,76 @@ function checkHandler(loader) {
|
|
|
4618
4574
|
};
|
|
4619
4575
|
};
|
|
4620
4576
|
}
|
|
4621
|
-
function
|
|
4622
|
-
const encoded = new TextEncoder().encode(JSON.stringify(plan3));
|
|
4623
|
-
let hash = 2166136261;
|
|
4624
|
-
for (const byte of encoded) {
|
|
4625
|
-
hash ^= byte;
|
|
4626
|
-
hash = Math.imul(hash, 16777619) >>> 0;
|
|
4627
|
-
}
|
|
4628
|
-
return hash.toString(16).padStart(8, "0");
|
|
4629
|
-
}
|
|
4630
|
-
function deployHandler(load, adapter, token) {
|
|
4577
|
+
function deployHandler() {
|
|
4631
4578
|
return async ({ args, cwd }) => {
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
inventory2 = await load({ projectRoot: cwd });
|
|
4638
|
-
} catch (error) {
|
|
4639
|
-
return deployPrecondition(error);
|
|
4579
|
+
if (args[0] !== "prepare") {
|
|
4580
|
+
return usage2(
|
|
4581
|
+
"deploy",
|
|
4582
|
+
"Expected hollow deploy prepare --target fly:<app> --database <sqlite|postgres> [--region <region>] [--force] [--json]."
|
|
4583
|
+
);
|
|
4640
4584
|
}
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
runner: {
|
|
4655
|
-
async run() {
|
|
4656
|
-
throw new Error("No Fly command runner was configured for this invocation.");
|
|
4657
|
-
}
|
|
4658
|
-
}
|
|
4659
|
-
}),
|
|
4660
|
-
() => (/* @__PURE__ */ new Date()).toISOString()
|
|
4661
|
-
);
|
|
4662
|
-
} catch (error) {
|
|
4663
|
-
return deployPrecondition(error).result;
|
|
4585
|
+
let target;
|
|
4586
|
+
let database;
|
|
4587
|
+
let region;
|
|
4588
|
+
let force = false;
|
|
4589
|
+
if (args.filter((argument) => argument === "--json").length > 1) {
|
|
4590
|
+
return usage2("deploy", "Deployment preparation accepts --json at most once.");
|
|
4591
|
+
}
|
|
4592
|
+
for (let index = 1; index < args.length; index++) {
|
|
4593
|
+
const argument = args[index];
|
|
4594
|
+
if (argument === "--json") continue;
|
|
4595
|
+
if (argument === "--force" && !force) {
|
|
4596
|
+
force = true;
|
|
4597
|
+
continue;
|
|
4664
4598
|
}
|
|
4599
|
+
if ((argument === "--target" || argument === "--database" || argument === "--region") && index + 1 < args.length) {
|
|
4600
|
+
const value = args[++index];
|
|
4601
|
+
if (argument === "--target" && !target) target = value;
|
|
4602
|
+
else if (argument === "--database" && !database && (value === "sqlite" || value === "postgres")) database = value;
|
|
4603
|
+
else if (argument === "--region" && !region) region = value;
|
|
4604
|
+
else return usage2("deploy", "Deployment preparation options must occur once with a valid value.");
|
|
4605
|
+
continue;
|
|
4606
|
+
}
|
|
4607
|
+
return usage2("deploy", "Deployment preparation options are invalid.");
|
|
4608
|
+
}
|
|
4609
|
+
if (!target?.startsWith("fly:") || !database) {
|
|
4610
|
+
return usage2("deploy", "Deployment preparation requires --target fly:<app> and --database sqlite or postgres.");
|
|
4611
|
+
}
|
|
4612
|
+
try {
|
|
4613
|
+
const prepared = await prepareFlyDeployment({
|
|
4614
|
+
projectRoot: cwd,
|
|
4615
|
+
target: { kind: "fly", app: target.slice("fly:".length) },
|
|
4616
|
+
database,
|
|
4617
|
+
...region ? { region } : {},
|
|
4618
|
+
...force ? { force: true } : {}
|
|
4619
|
+
});
|
|
4665
4620
|
return {
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
correction: item.correction
|
|
4675
|
-
})),
|
|
4676
|
-
json: JSON.parse(renderJsonDeployResult(result))
|
|
4621
|
+
result: {
|
|
4622
|
+
ok: true,
|
|
4623
|
+
command: "deploy",
|
|
4624
|
+
schema: prepared.schema,
|
|
4625
|
+
summary: renderFlyPreparation(prepared).trimEnd(),
|
|
4626
|
+
diagnostics: [],
|
|
4627
|
+
json: prepared
|
|
4628
|
+
}
|
|
4677
4629
|
};
|
|
4678
|
-
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4630
|
+
} catch (error) {
|
|
4631
|
+
const diagnostics = error instanceof DeploymentPreparationError ? error.diagnostics : [{
|
|
4632
|
+
code: "SH_DEPLOY_PREPARE_FAILED",
|
|
4633
|
+
summary: "Deployment preparation failed.",
|
|
4634
|
+
correction: "Inspect the project files and retry the documented command."
|
|
4635
|
+
}];
|
|
4681
4636
|
return {
|
|
4637
|
+
exitCode: 1,
|
|
4682
4638
|
result: {
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
confirmationDigest: digest2,
|
|
4689
|
-
...provided ? { providedConfirmation: provided } : {},
|
|
4690
|
-
apply: () => run(true)
|
|
4639
|
+
ok: false,
|
|
4640
|
+
command: "deploy",
|
|
4641
|
+
schema: "sleepy-hollow-deploy-prepare-result/v1",
|
|
4642
|
+
summary: diagnostics[0].summary,
|
|
4643
|
+
diagnostics: diagnostics.map((item) => ({ ...item, severity: "error" }))
|
|
4691
4644
|
}
|
|
4692
4645
|
};
|
|
4693
4646
|
}
|
|
4694
|
-
const applied = await run(true);
|
|
4695
|
-
return { exitCode: applied.ok ? 0 : 1, result: applied };
|
|
4696
|
-
};
|
|
4697
|
-
}
|
|
4698
|
-
function deployPrecondition(error) {
|
|
4699
|
-
const summary = error instanceof Error ? error.message : "Deployment could not start.";
|
|
4700
|
-
return {
|
|
4701
|
-
exitCode: 1,
|
|
4702
|
-
result: {
|
|
4703
|
-
ok: false,
|
|
4704
|
-
command: "deploy",
|
|
4705
|
-
schema: "sleepy-hollow-deploy-result/v1",
|
|
4706
|
-
summary,
|
|
4707
|
-
diagnostics: [{
|
|
4708
|
-
code: "SH_DEPLOY_PRECONDITION_UNMET",
|
|
4709
|
-
severity: "error",
|
|
4710
|
-
summary,
|
|
4711
|
-
correction: "Resolve the reported condition and rerun hollow deploy."
|
|
4712
|
-
}]
|
|
4713
|
-
}
|
|
4714
|
-
};
|
|
4715
|
-
}
|
|
4716
|
-
function unavailable(command) {
|
|
4717
|
-
return ({ args }) => {
|
|
4718
|
-
if (args.some((argument) => argument !== "--json")) {
|
|
4719
|
-
return usage2(
|
|
4720
|
-
command,
|
|
4721
|
-
`${command} options are unavailable until its canonical feature is installed.`
|
|
4722
|
-
);
|
|
4723
|
-
}
|
|
4724
|
-
return {
|
|
4725
|
-
result: {
|
|
4726
|
-
ok: false,
|
|
4727
|
-
command,
|
|
4728
|
-
schema: `sleepy-hollow-${command}-result/v1`,
|
|
4729
|
-
summary: `${command} is not available in this build.`,
|
|
4730
|
-
diagnostics: [{
|
|
4731
|
-
code: "SH_CLI_FEATURE_UNAVAILABLE",
|
|
4732
|
-
severity: "error",
|
|
4733
|
-
summary: `The canonical ${command} command API is not implemented.`,
|
|
4734
|
-
correction: "Install a build containing the governed canonical feature."
|
|
4735
|
-
}]
|
|
4736
|
-
}
|
|
4737
|
-
};
|
|
4738
4647
|
};
|
|
4739
4648
|
}
|
|
4740
4649
|
function testHandler(loader, runner) {
|
|
@@ -4800,11 +4709,7 @@ function createCliHandlers(dependencies = {}) {
|
|
|
4800
4709
|
),
|
|
4801
4710
|
check: checkHandler(dependencies.checkInventoryLoader),
|
|
4802
4711
|
generate: generate2,
|
|
4803
|
-
deploy:
|
|
4804
|
-
dependencies.deployInventoryLoader,
|
|
4805
|
-
dependencies.deployAdapter,
|
|
4806
|
-
dependencies.deployToken
|
|
4807
|
-
) : unavailable("deploy")
|
|
4712
|
+
deploy: deployHandler()
|
|
4808
4713
|
};
|
|
4809
4714
|
return Object.fromEntries(CLI_COMMANDS.map((command) => [
|
|
4810
4715
|
command,
|
|
@@ -4972,7 +4877,7 @@ async function locations(options) {
|
|
|
4972
4877
|
}
|
|
4973
4878
|
|
|
4974
4879
|
// skills/sleepy-hollow/planning/parser.ts
|
|
4975
|
-
import { createHash as
|
|
4880
|
+
import { createHash as createHash4 } from "crypto";
|
|
4976
4881
|
import { parse as parse4 } from "yaml";
|
|
4977
4882
|
|
|
4978
4883
|
// skills/sleepy-hollow/planning/planning_error.ts
|
|
@@ -5058,13 +4963,13 @@ function frontmatterOf(source, path) {
|
|
|
5058
4963
|
schema: "core"
|
|
5059
4964
|
});
|
|
5060
4965
|
} catch (error) {
|
|
5061
|
-
const
|
|
5062
|
-
const message =
|
|
4966
|
+
const failure3 = error;
|
|
4967
|
+
const message = failure3.message ?? String(error);
|
|
5063
4968
|
diagnostics.push(diagnostic5(
|
|
5064
4969
|
/duplicat/i.test(message) ? "SH_PLANNING_YAML_DUPLICATE_KEY" : "SH_PLANNING_YAML_INVALID",
|
|
5065
4970
|
path,
|
|
5066
|
-
(
|
|
5067
|
-
(
|
|
4971
|
+
(failure3.mark?.line ?? 0) + 2,
|
|
4972
|
+
(failure3.mark?.column ?? 0) + 1,
|
|
5068
4973
|
`Invalid requirement frontmatter: ${message}`,
|
|
5069
4974
|
"Correct the YAML frontmatter without adding proprietary syntax."
|
|
5070
4975
|
));
|
|
@@ -5229,9 +5134,9 @@ function digestOf(source, path) {
|
|
|
5229
5134
|
)]);
|
|
5230
5135
|
}
|
|
5231
5136
|
const normalized = normalizedFrontmatter + governed.slice(frontmatter.length);
|
|
5232
|
-
return
|
|
5137
|
+
return createHash4("sha256").update(normalized).digest("hex");
|
|
5233
5138
|
}
|
|
5234
|
-
function approvalOf(source,
|
|
5139
|
+
function approvalOf(source, digest3, criteria) {
|
|
5235
5140
|
const governance = source.slice(source.indexOf("## Governance record"));
|
|
5236
5141
|
const approvalStart = governance.indexOf("### Approval");
|
|
5237
5142
|
if (approvalStart < 0) return void 0;
|
|
@@ -5250,7 +5155,7 @@ function approvalOf(source, digest2, criteria) {
|
|
|
5250
5155
|
(item) => criterionText.includes(item.id)
|
|
5251
5156
|
).map((item) => item.id);
|
|
5252
5157
|
const valid = Boolean(
|
|
5253
|
-
approver && approvedAt && decisionSource && recordedDigest ===
|
|
5158
|
+
approver && approvedAt && decisionSource && recordedDigest === digest3 && criteria.length > 0 && approvedCriteria.length === criteria.length
|
|
5254
5159
|
);
|
|
5255
5160
|
return {
|
|
5256
5161
|
approver: approver ?? "",
|
|
@@ -5495,7 +5400,7 @@ async function collect(absoluteRoot, displayRoot, serviceId2, options) {
|
|
|
5495
5400
|
return found;
|
|
5496
5401
|
}
|
|
5497
5402
|
async function requirements(project, options) {
|
|
5498
|
-
const
|
|
5403
|
+
const read2 = options.readTextFile ?? ((path) => platform.readTextFile(path));
|
|
5499
5404
|
const roots = project.services.length > 0 ? project.services.map((service) => ({
|
|
5500
5405
|
absolute: `${project.projectRoot}/${service.apiRoot}`,
|
|
5501
5406
|
display: service.apiRoot,
|
|
@@ -5527,7 +5432,7 @@ async function requirements(project, options) {
|
|
|
5527
5432
|
}
|
|
5528
5433
|
let source;
|
|
5529
5434
|
try {
|
|
5530
|
-
source = await
|
|
5435
|
+
source = await read2(file.absolutePath);
|
|
5531
5436
|
} catch {
|
|
5532
5437
|
diagnostics.push(issue2(
|
|
5533
5438
|
"SH_EVIDENCE_REQUIREMENT_UNREADABLE",
|
|
@@ -5617,10 +5522,10 @@ async function requirements(project, options) {
|
|
|
5617
5522
|
var CAPTURE_FILE = "capture.json";
|
|
5618
5523
|
async function capture(project, options) {
|
|
5619
5524
|
const path = `${project.projectRoot}/${project.generatedDirectory}/${CAPTURE_FILE}`;
|
|
5620
|
-
const
|
|
5525
|
+
const read2 = options.readTextFile ?? ((target) => platform.readTextFile(target));
|
|
5621
5526
|
let source;
|
|
5622
5527
|
try {
|
|
5623
|
-
source = await
|
|
5528
|
+
source = await read2(path);
|
|
5624
5529
|
} catch {
|
|
5625
5530
|
throw new EvidenceError([{
|
|
5626
5531
|
code: "SH_EVIDENCE_CAPTURE_MISSING",
|
|
@@ -5804,25 +5709,6 @@ async function checkLoader(projectRoot, revision2, _scope) {
|
|
|
5804
5709
|
}
|
|
5805
5710
|
};
|
|
5806
5711
|
}
|
|
5807
|
-
async function deployLoader(projectRoot, options) {
|
|
5808
|
-
const project = await locations({ projectRoot });
|
|
5809
|
-
const verification = verifyProject(
|
|
5810
|
-
await checkLoader(projectRoot, options.revision, void 0)
|
|
5811
|
-
);
|
|
5812
|
-
return {
|
|
5813
|
-
projectRootDisplay: projectRoot,
|
|
5814
|
-
target: options.target,
|
|
5815
|
-
revision: options.revision,
|
|
5816
|
-
verification,
|
|
5817
|
-
environmentKeys: [],
|
|
5818
|
-
deployedEnvironmentKeys: [],
|
|
5819
|
-
contractChanges: [],
|
|
5820
|
-
openApiPath: `${project.generatedDirectory}/openapi.json`,
|
|
5821
|
-
documentationPath: `${project.generatedDirectory}/docs.html`,
|
|
5822
|
-
smokeTests: [],
|
|
5823
|
-
firstExternalDeployment: true
|
|
5824
|
-
};
|
|
5825
|
-
}
|
|
5826
5712
|
async function testLoader(projectRoot) {
|
|
5827
5713
|
const project = await locations({ projectRoot });
|
|
5828
5714
|
const governed = await requirements(project, { projectRoot });
|
|
@@ -5857,12 +5743,6 @@ function createCheckInventoryLoader(options) {
|
|
|
5857
5743
|
function createTestInventoryLoader() {
|
|
5858
5744
|
return (request) => testLoader(request.projectRoot);
|
|
5859
5745
|
}
|
|
5860
|
-
function createDeployInventoryLoader(options) {
|
|
5861
|
-
return (request) => deployLoader(request.projectRoot, {
|
|
5862
|
-
revision: typeof options.revision === "function" ? options.revision() : options.revision,
|
|
5863
|
-
target: options.target
|
|
5864
|
-
});
|
|
5865
|
-
}
|
|
5866
5746
|
|
|
5867
5747
|
// cli/main.ts
|
|
5868
5748
|
var VERSION2 = CLI_VERSION;
|
|
@@ -5875,17 +5755,18 @@ function revision() {
|
|
|
5875
5755
|
}
|
|
5876
5756
|
};
|
|
5877
5757
|
}
|
|
5878
|
-
function
|
|
5758
|
+
function runCli(args, io, dependencies = {}) {
|
|
5759
|
+
return runCommandSurface(args, io, createCliHandlers(dependencies));
|
|
5760
|
+
}
|
|
5761
|
+
function invokedAsCli(entry = process.argv[1]) {
|
|
5762
|
+
if (!entry) return false;
|
|
5879
5763
|
try {
|
|
5880
|
-
return
|
|
5764
|
+
return import.meta.url === pathToFileURL4(realpathSync(entry)).href;
|
|
5881
5765
|
} catch {
|
|
5882
|
-
return
|
|
5766
|
+
return false;
|
|
5883
5767
|
}
|
|
5884
5768
|
}
|
|
5885
|
-
|
|
5886
|
-
return runCommandSurface(args, io, createCliHandlers(dependencies));
|
|
5887
|
-
}
|
|
5888
|
-
if (process.argv[1] && import.meta.url === pathToFileURL4(process.argv[1]).href) {
|
|
5769
|
+
if (invokedAsCli()) {
|
|
5889
5770
|
if (platform.args[0] === WORKER && platform.env.get("SLEEPY_HOLLOW_INTERNAL_DEV_WORKER") === "1") {
|
|
5890
5771
|
platform.exit(await runDevWorker(platform.args.slice(1)));
|
|
5891
5772
|
}
|
|
@@ -5895,11 +5776,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL4(process.argv[1]).href)
|
|
|
5895
5776
|
stderr: console.error
|
|
5896
5777
|
}, {
|
|
5897
5778
|
checkInventoryLoader: createCheckInventoryLoader({ revision: revision() }),
|
|
5898
|
-
testInventoryLoader: createTestInventoryLoader()
|
|
5899
|
-
deployInventoryLoader: createDeployInventoryLoader({
|
|
5900
|
-
revision: revision(),
|
|
5901
|
-
target: { kind: "fly", project: projectName() }
|
|
5902
|
-
})
|
|
5779
|
+
testInventoryLoader: createTestInventoryLoader()
|
|
5903
5780
|
});
|
|
5904
5781
|
platform.exit(code);
|
|
5905
5782
|
}
|