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