@forgezero/agent 0.1.30 → 0.1.31
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/README.md +28 -0
- package/dist/agent-heartbeat.js +1 -1
- package/dist/definition.d.ts +5 -2
- package/dist/definition.js +32 -21
- package/dist/deploy-file.d.ts +40 -0
- package/dist/deploy-file.js +378 -0
- package/dist/deployment.d.ts +4 -0
- package/dist/fz-agent.js +59 -28
- package/dist/fz.js +417 -19
- package/dist/provision.js +4 -4
- package/dist/software-helper.js +3 -3
- package/dist/software.d.ts +1 -1
- package/dist/software.js +3 -3
- package/dist/version.d.ts +1 -1
- package/package.json +8 -2
- package/schema/deploy-v2.json +70 -0
package/dist/fz.js
CHANGED
|
@@ -4871,7 +4871,7 @@ async function spawnWith(command, env, report = () => {}) {
|
|
|
4871
4871
|
|
|
4872
4872
|
// src/cli/index.ts
|
|
4873
4873
|
init_dist();
|
|
4874
|
-
import { existsSync as
|
|
4874
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, statSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
4875
4875
|
import { dirname as dirname2 } from "path";
|
|
4876
4876
|
import { fileURLToPath } from "url";
|
|
4877
4877
|
|
|
@@ -4886,12 +4886,22 @@ var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-
|
|
|
4886
4886
|
var MAX_REQUEST_BYTES = 8 * 1024;
|
|
4887
4887
|
|
|
4888
4888
|
// src/version.ts
|
|
4889
|
-
var VERSION2 = "0.1.
|
|
4889
|
+
var VERSION2 = "0.1.31";
|
|
4890
4890
|
|
|
4891
4891
|
// src/software.ts
|
|
4892
4892
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4893
4893
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
4894
4894
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
4895
|
+
var OS_CATALOG = [
|
|
4896
|
+
{ id: "ubuntu", version: "26.04", architecture: "x64", status: "active" }
|
|
4897
|
+
];
|
|
4898
|
+
var SOFTWARE_CATALOG = [
|
|
4899
|
+
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4900
|
+
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4901
|
+
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4902
|
+
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4903
|
+
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4904
|
+
];
|
|
4895
4905
|
var UBUNTU_2604_X64 = [
|
|
4896
4906
|
{
|
|
4897
4907
|
requirement: { id: "bun", version: "1.3.14" },
|
|
@@ -4919,6 +4929,31 @@ var UBUNTU_2604_X64 = [
|
|
|
4919
4929
|
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
|
|
4920
4930
|
}
|
|
4921
4931
|
];
|
|
4932
|
+
function validateSoftwareRequirements(value, _options = {}) {
|
|
4933
|
+
if (!Array.isArray(value) || value.length > 32)
|
|
4934
|
+
throw new Error("software requirements must be an array of at most 32 entries");
|
|
4935
|
+
const seen = new Set;
|
|
4936
|
+
return value.map((item) => {
|
|
4937
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
4938
|
+
throw new Error("software requirement must be an object");
|
|
4939
|
+
const row = item;
|
|
4940
|
+
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
4941
|
+
throw new Error("software requirement contains an unknown field");
|
|
4942
|
+
}
|
|
4943
|
+
if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
4944
|
+
throw new Error("software requirement coordinate is invalid");
|
|
4945
|
+
}
|
|
4946
|
+
const requirement = { id: row.id, version: row.version };
|
|
4947
|
+
if (seen.has(requirement.id))
|
|
4948
|
+
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
4949
|
+
seen.add(requirement.id);
|
|
4950
|
+
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
4951
|
+
if (!catalog || catalog.status !== "active") {
|
|
4952
|
+
throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
|
|
4953
|
+
}
|
|
4954
|
+
return requirement;
|
|
4955
|
+
});
|
|
4956
|
+
}
|
|
4922
4957
|
|
|
4923
4958
|
// src/software-helper.ts
|
|
4924
4959
|
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
@@ -9530,6 +9565,262 @@ function checkProjectContext(rootInput) {
|
|
|
9530
9565
|
return { ok: problems.length === 0, problems };
|
|
9531
9566
|
}
|
|
9532
9567
|
|
|
9568
|
+
// src/deploy-file.ts
|
|
9569
|
+
import { createHash as createHash2 } from "crypto";
|
|
9570
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
9571
|
+
import { basename, join as join3 } from "path";
|
|
9572
|
+
|
|
9573
|
+
// src/definition.ts
|
|
9574
|
+
var PIPELINE_VERSION = 2;
|
|
9575
|
+
var DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v2.json";
|
|
9576
|
+
|
|
9577
|
+
class DefinitionError extends Error {
|
|
9578
|
+
constructor(message) {
|
|
9579
|
+
super(message);
|
|
9580
|
+
this.name = "DefinitionError";
|
|
9581
|
+
}
|
|
9582
|
+
}
|
|
9583
|
+
var record = (value, where) => {
|
|
9584
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9585
|
+
throw new DefinitionError(`${where} must be an object.`);
|
|
9586
|
+
}
|
|
9587
|
+
return value;
|
|
9588
|
+
};
|
|
9589
|
+
var text2 = (value, where) => {
|
|
9590
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
9591
|
+
throw new DefinitionError(`${where} must be a non-empty string.`);
|
|
9592
|
+
}
|
|
9593
|
+
return value;
|
|
9594
|
+
};
|
|
9595
|
+
var exactKeys = (value, allowed, where) => {
|
|
9596
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
9597
|
+
if (unknown.length > 0)
|
|
9598
|
+
throw new DefinitionError(`${where} contains unknown field(s): ${unknown.join(", ")}.`);
|
|
9599
|
+
};
|
|
9600
|
+
var NAME2 = /^[a-z][a-z0-9-]{0,62}$/;
|
|
9601
|
+
var RESERVED_STEP_ENV = new Set([
|
|
9602
|
+
"PATH",
|
|
9603
|
+
"HOME",
|
|
9604
|
+
"SHELL",
|
|
9605
|
+
"PWD",
|
|
9606
|
+
"BUN_INSTALL",
|
|
9607
|
+
"NODE_OPTIONS",
|
|
9608
|
+
"LD_PRELOAD",
|
|
9609
|
+
"LD_LIBRARY_PATH",
|
|
9610
|
+
"GIT_SSH",
|
|
9611
|
+
"GIT_SSH_COMMAND"
|
|
9612
|
+
]);
|
|
9613
|
+
function parseDeployDefinition(value, options = {}) {
|
|
9614
|
+
const root = record(value, "pipeline");
|
|
9615
|
+
exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
|
|
9616
|
+
if (root.$schema !== undefined && root.$schema !== DEPLOY_SCHEMA_URL) {
|
|
9617
|
+
throw new DefinitionError(`pipeline.$schema must be ${DEPLOY_SCHEMA_URL}.`);
|
|
9618
|
+
}
|
|
9619
|
+
if (root.version !== PIPELINE_VERSION) {
|
|
9620
|
+
throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
|
|
9621
|
+
}
|
|
9622
|
+
if (root.requireAttestation !== undefined && typeof root.requireAttestation !== "boolean") {
|
|
9623
|
+
throw new DefinitionError("pipeline.requireAttestation must be a boolean.");
|
|
9624
|
+
}
|
|
9625
|
+
const rawProfiles = record(root.profiles, "pipeline.profiles");
|
|
9626
|
+
const profileEntries = Object.entries(rawProfiles);
|
|
9627
|
+
if (profileEntries.length === 0 || profileEntries.length > 32) {
|
|
9628
|
+
throw new DefinitionError("pipeline.profiles must contain from 1 to 32 named profiles.");
|
|
9629
|
+
}
|
|
9630
|
+
if (!Array.isArray(root.steps) || root.steps.length === 0) {
|
|
9631
|
+
throw new DefinitionError("pipeline.steps must contain at least one step.");
|
|
9632
|
+
}
|
|
9633
|
+
const profiles = {};
|
|
9634
|
+
for (const [name2, raw] of profileEntries) {
|
|
9635
|
+
if (!NAME2.test(name2))
|
|
9636
|
+
throw new DefinitionError(`pipeline profile name is invalid: ${name2}.`);
|
|
9637
|
+
const profile = record(raw, `profiles.${name2}`);
|
|
9638
|
+
exactKeys(profile, ["software"], `profiles.${name2}`);
|
|
9639
|
+
if (!Array.isArray(profile.software)) {
|
|
9640
|
+
throw new DefinitionError(`profiles.${name2}.software must be an array.`);
|
|
9641
|
+
}
|
|
9642
|
+
profiles[name2] = { software: validateSoftwareRequirements(profile.software, options) };
|
|
9643
|
+
}
|
|
9644
|
+
const phases = new Set(["build", "release", "migrate", "health"]);
|
|
9645
|
+
const steps = root.steps.map((raw, index) => {
|
|
9646
|
+
const step = record(raw, `steps[${index}]`);
|
|
9647
|
+
exactKeys(step, ["name", "run", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
|
|
9648
|
+
const phase = text2(step.phase, `steps[${index}].phase`);
|
|
9649
|
+
if (!phases.has(phase))
|
|
9650
|
+
throw new DefinitionError(`steps[${index}].phase is not supported.`);
|
|
9651
|
+
if (step.scope !== "target" && step.scope !== "release") {
|
|
9652
|
+
throw new DefinitionError(`steps[${index}].scope must be target or release.`);
|
|
9653
|
+
}
|
|
9654
|
+
if (step.always !== undefined && typeof step.always !== "boolean") {
|
|
9655
|
+
throw new DefinitionError(`steps[${index}].always must be a boolean.`);
|
|
9656
|
+
}
|
|
9657
|
+
let selectedProfiles;
|
|
9658
|
+
if (step.profiles !== undefined) {
|
|
9659
|
+
if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name2) => typeof name2 !== "string" || !Object.hasOwn(profiles, name2))) {
|
|
9660
|
+
throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
|
|
9661
|
+
}
|
|
9662
|
+
selectedProfiles = [...step.profiles];
|
|
9663
|
+
if (new Set(selectedProfiles).size !== selectedProfiles.length) {
|
|
9664
|
+
throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
|
|
9665
|
+
}
|
|
9666
|
+
}
|
|
9667
|
+
if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name2) => typeof name2 !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name2)))) {
|
|
9668
|
+
throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
|
|
9669
|
+
}
|
|
9670
|
+
if (Array.isArray(step.secrets) && new Set(step.secrets).size !== step.secrets.length) {
|
|
9671
|
+
throw new DefinitionError(`steps[${index}].secrets must not contain duplicates.`);
|
|
9672
|
+
}
|
|
9673
|
+
if (Array.isArray(step.secrets) && step.secrets.some((name2) => RESERVED_STEP_ENV.has(String(name2)))) {
|
|
9674
|
+
throw new DefinitionError(`steps[${index}].secrets may not replace process-control environment variables.`);
|
|
9675
|
+
}
|
|
9676
|
+
const timeoutMs = step.timeoutMs === undefined ? undefined : Number(step.timeoutMs);
|
|
9677
|
+
if (timeoutMs !== undefined && (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86400000)) {
|
|
9678
|
+
throw new DefinitionError(`steps[${index}].timeoutMs must be an integer from 1 to 86400000.`);
|
|
9679
|
+
}
|
|
9680
|
+
let when;
|
|
9681
|
+
if (step.when !== undefined) {
|
|
9682
|
+
const conditions = record(step.when, `steps[${index}].when`);
|
|
9683
|
+
when = {};
|
|
9684
|
+
for (const [name2, expected] of Object.entries(conditions)) {
|
|
9685
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(name2) || typeof expected !== "string" || expected.length === 0) {
|
|
9686
|
+
throw new DefinitionError(`steps[${index}].when must map environment names to non-empty strings.`);
|
|
9687
|
+
}
|
|
9688
|
+
when[name2] = expected;
|
|
9689
|
+
}
|
|
9690
|
+
if (Object.keys(when).length === 0)
|
|
9691
|
+
throw new DefinitionError(`steps[${index}].when must not be empty.`);
|
|
9692
|
+
}
|
|
9693
|
+
return {
|
|
9694
|
+
name: text2(step.name, `steps[${index}].name`),
|
|
9695
|
+
run: text2(step.run, `steps[${index}].run`),
|
|
9696
|
+
phase,
|
|
9697
|
+
scope: step.scope,
|
|
9698
|
+
profiles: selectedProfiles,
|
|
9699
|
+
secrets: step.secrets,
|
|
9700
|
+
always: step.always === true,
|
|
9701
|
+
timeoutMs,
|
|
9702
|
+
when
|
|
9703
|
+
};
|
|
9704
|
+
});
|
|
9705
|
+
if (new Set(steps.map((step) => step.name)).size !== steps.length) {
|
|
9706
|
+
throw new DefinitionError("pipeline.steps must have unique names.");
|
|
9707
|
+
}
|
|
9708
|
+
const name = text2(root.name, "pipeline.name");
|
|
9709
|
+
if (name.length > 120)
|
|
9710
|
+
throw new DefinitionError("pipeline.name must be at most 120 characters.");
|
|
9711
|
+
return {
|
|
9712
|
+
version: PIPELINE_VERSION,
|
|
9713
|
+
name,
|
|
9714
|
+
requireAttestation: root.requireAttestation === true,
|
|
9715
|
+
profiles,
|
|
9716
|
+
steps
|
|
9717
|
+
};
|
|
9718
|
+
}
|
|
9719
|
+
|
|
9720
|
+
// src/deploy-file.ts
|
|
9721
|
+
var DEPLOY_FILE = ".fz/deploy.json";
|
|
9722
|
+
var DEPLOY_TODO_PREFIX = "ForgeZero pipeline TODO:";
|
|
9723
|
+
var stable = (value) => {
|
|
9724
|
+
if (Array.isArray(value))
|
|
9725
|
+
return `[${value.map(stable).join(",")}]`;
|
|
9726
|
+
if (value && typeof value === "object") {
|
|
9727
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stable(entry)}`).join(",")}}`;
|
|
9728
|
+
}
|
|
9729
|
+
return JSON.stringify(value);
|
|
9730
|
+
};
|
|
9731
|
+
function deployDefinitionDigest(definition) {
|
|
9732
|
+
return `sha256:${createHash2("sha256").update(stable(definition)).digest("hex")}`;
|
|
9733
|
+
}
|
|
9734
|
+
var safeName = (value) => {
|
|
9735
|
+
const normalized = value.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
9736
|
+
return /^[a-z]/.test(normalized) ? normalized : `app-${normalized || "service"}`.slice(0, 63);
|
|
9737
|
+
};
|
|
9738
|
+
function packageHints(root) {
|
|
9739
|
+
const packagePath = join3(root, "package.json");
|
|
9740
|
+
if (!existsSync2(packagePath))
|
|
9741
|
+
return { bun: existsSync2(join3(root, "bun.lock")) };
|
|
9742
|
+
try {
|
|
9743
|
+
const manifest = JSON.parse(readFileSync2(packagePath, "utf8"));
|
|
9744
|
+
return {
|
|
9745
|
+
name: typeof manifest.name === "string" ? manifest.name : undefined,
|
|
9746
|
+
build: typeof manifest.scripts?.build === "string" ? "bun run build" : undefined,
|
|
9747
|
+
bun: existsSync2(join3(root, "bun.lock")) || existsSync2(join3(root, "bun.lockb"))
|
|
9748
|
+
};
|
|
9749
|
+
} catch {
|
|
9750
|
+
return { bun: existsSync2(join3(root, "bun.lock")) };
|
|
9751
|
+
}
|
|
9752
|
+
}
|
|
9753
|
+
var blocker = (instruction) => `printf '%s\\n' '${DEPLOY_TODO_PREFIX} ${instruction}' >&2; exit 78`;
|
|
9754
|
+
function defaultDeployFile(root, options = {}) {
|
|
9755
|
+
const hints = packageHints(root);
|
|
9756
|
+
const software = options.software ?? (hints.bun ? SOFTWARE_CATALOG.filter((entry) => entry.id === "bun" && entry.status === "active").map(({ id: id2, version }) => ({ id: id2, version })) : []);
|
|
9757
|
+
validateSoftwareRequirements(software, { channel: options.channel });
|
|
9758
|
+
const profile = options.profile ?? "app";
|
|
9759
|
+
const build = hints.build ?? blocker("replace the build step with the project build command");
|
|
9760
|
+
return {
|
|
9761
|
+
$schema: DEPLOY_SCHEMA_URL,
|
|
9762
|
+
version: 2,
|
|
9763
|
+
name: safeName(options.name ?? hints.name ?? basename(root)),
|
|
9764
|
+
...options.requireAttestation ? { requireAttestation: true } : {},
|
|
9765
|
+
profiles: { [profile]: { software } },
|
|
9766
|
+
steps: [
|
|
9767
|
+
{ name: "build", phase: "build", scope: "target", run: build, timeoutMs: 600000 },
|
|
9768
|
+
{
|
|
9769
|
+
name: "promote release",
|
|
9770
|
+
phase: "release",
|
|
9771
|
+
scope: "target",
|
|
9772
|
+
run: blocker("replace the release step with an atomic promotion command"),
|
|
9773
|
+
timeoutMs: 120000
|
|
9774
|
+
},
|
|
9775
|
+
{
|
|
9776
|
+
name: "health check",
|
|
9777
|
+
phase: "health",
|
|
9778
|
+
scope: "target",
|
|
9779
|
+
run: blocker("replace the health step with a bounded local health check"),
|
|
9780
|
+
timeoutMs: 30000
|
|
9781
|
+
}
|
|
9782
|
+
]
|
|
9783
|
+
};
|
|
9784
|
+
}
|
|
9785
|
+
function inspectDeployFile(root, options = {}) {
|
|
9786
|
+
const path = join3(root, DEPLOY_FILE);
|
|
9787
|
+
if (!existsSync2(path))
|
|
9788
|
+
throw new Error(`${DEPLOY_FILE} does not exist; run \`fz deploy init\`.`);
|
|
9789
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
9790
|
+
const definition = parseDeployDefinition(raw, options);
|
|
9791
|
+
const problems = definition.steps.filter((step) => step.run.includes(DEPLOY_TODO_PREFIX)).map((step) => `${step.name} still contains the safe initialization blocker`);
|
|
9792
|
+
const profiles = Object.keys(definition.profiles).sort();
|
|
9793
|
+
return {
|
|
9794
|
+
path,
|
|
9795
|
+
definition,
|
|
9796
|
+
summary: {
|
|
9797
|
+
path,
|
|
9798
|
+
digest: deployDefinitionDigest(definition),
|
|
9799
|
+
version: definition.version,
|
|
9800
|
+
name: definition.name,
|
|
9801
|
+
profiles,
|
|
9802
|
+
software: Object.fromEntries(profiles.map((profile) => [
|
|
9803
|
+
profile,
|
|
9804
|
+
definition.profiles[profile].software
|
|
9805
|
+
])),
|
|
9806
|
+
ready: problems.length === 0,
|
|
9807
|
+
problems
|
|
9808
|
+
}
|
|
9809
|
+
};
|
|
9810
|
+
}
|
|
9811
|
+
function initializeDeployFile(root, options = {}) {
|
|
9812
|
+
const path = join3(root, DEPLOY_FILE);
|
|
9813
|
+
if (existsSync2(path) && !options.force) {
|
|
9814
|
+
throw new Error(`${DEPLOY_FILE} already exists; use --force only when replacing it deliberately.`);
|
|
9815
|
+
}
|
|
9816
|
+
const raw = defaultDeployFile(root, options);
|
|
9817
|
+
parseDeployDefinition(raw, { channel: options.channel });
|
|
9818
|
+
mkdirSync2(join3(root, ".fz"), { recursive: true });
|
|
9819
|
+
writeFileSync2(path, `${JSON.stringify(raw, null, 2)}
|
|
9820
|
+
`, { mode: 420 });
|
|
9821
|
+
return inspectDeployFile(root, { channel: options.channel });
|
|
9822
|
+
}
|
|
9823
|
+
|
|
9533
9824
|
// src/cli/index.ts
|
|
9534
9825
|
var DEFAULT_MODE = THRESHOLD_MODES[0].id;
|
|
9535
9826
|
var RECOMMENDED_MODE = (THRESHOLD_MODES.find((mode) => mode.recommended) ?? THRESHOLD_MODES[0]).id;
|
|
@@ -9547,6 +9838,10 @@ function parseOptions(argv) {
|
|
|
9547
9838
|
email: process.env.FZ_EMAIL ?? "operator@localhost",
|
|
9548
9839
|
preserveEnv: false,
|
|
9549
9840
|
projectRoot: process.cwd(),
|
|
9841
|
+
deployProfile: "app",
|
|
9842
|
+
deploySoftware: [],
|
|
9843
|
+
deployChannel: "production",
|
|
9844
|
+
requireAttestation: false,
|
|
9550
9845
|
force: false
|
|
9551
9846
|
};
|
|
9552
9847
|
const positional = [];
|
|
@@ -9572,6 +9867,18 @@ function parseOptions(argv) {
|
|
|
9572
9867
|
options.projectName = argv[++index];
|
|
9573
9868
|
else if (token === "--purpose")
|
|
9574
9869
|
options.projectPurpose = argv[++index];
|
|
9870
|
+
else if (token === "--profile")
|
|
9871
|
+
options.deployProfile = argv[++index] ?? options.deployProfile;
|
|
9872
|
+
else if (token === "--software")
|
|
9873
|
+
options.deploySoftware.push(argv[++index] ?? "");
|
|
9874
|
+
else if (token === "--channel") {
|
|
9875
|
+
const channel = argv[++index];
|
|
9876
|
+
if (channel === "development" || channel === "production")
|
|
9877
|
+
options.deployChannel = channel;
|
|
9878
|
+
else
|
|
9879
|
+
options.optionError = "--channel must be production or development.";
|
|
9880
|
+
} else if (token === "--attestation")
|
|
9881
|
+
options.requireAttestation = true;
|
|
9575
9882
|
else if (token === "--force")
|
|
9576
9883
|
options.force = true;
|
|
9577
9884
|
else if (token === "--key")
|
|
@@ -9592,15 +9899,15 @@ function parseOptions(argv) {
|
|
|
9592
9899
|
return { command: positional[0] ?? "help", args: positional.slice(1), options };
|
|
9593
9900
|
}
|
|
9594
9901
|
var out = {
|
|
9595
|
-
line: (
|
|
9902
|
+
line: (text3 = "") => process.stdout.write(`${text3}
|
|
9596
9903
|
`),
|
|
9597
|
-
step: (
|
|
9904
|
+
step: (text3) => process.stdout.write(` ${text3}
|
|
9598
9905
|
`),
|
|
9599
|
-
warn: (
|
|
9906
|
+
warn: (text3) => process.stderr.write(` ! ${text3}
|
|
9600
9907
|
`),
|
|
9601
|
-
fail: (
|
|
9908
|
+
fail: (text3) => process.stderr.write(` \u2717 ${text3}
|
|
9602
9909
|
`),
|
|
9603
|
-
ok: (
|
|
9910
|
+
ok: (text3) => process.stdout.write(` \u2713 ${text3}
|
|
9604
9911
|
`)
|
|
9605
9912
|
};
|
|
9606
9913
|
var sessionCookie = null;
|
|
@@ -9805,17 +10112,17 @@ async function cmdAgent(options, args) {
|
|
|
9805
10112
|
return 0;
|
|
9806
10113
|
}
|
|
9807
10114
|
try {
|
|
9808
|
-
|
|
10115
|
+
writeFileSync3(plan.unitPath, plan.unit, { mode: 420 });
|
|
9809
10116
|
out.ok(`Wrote ${plan.unitPath}`);
|
|
9810
10117
|
for (const auxiliary of plan.auxiliaryUnits) {
|
|
9811
|
-
|
|
9812
|
-
|
|
10118
|
+
mkdirSync3(dirname2(auxiliary.path), { recursive: true, mode: 493 });
|
|
10119
|
+
writeFileSync3(auxiliary.path, auxiliary.unit, { mode: 420 });
|
|
9813
10120
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
9814
10121
|
}
|
|
9815
10122
|
if (options.enrol) {
|
|
9816
|
-
if (
|
|
10123
|
+
if (existsSync3(enrolTokenSourcePath)) {
|
|
9817
10124
|
const source = statSync(enrolTokenSourcePath);
|
|
9818
|
-
const token =
|
|
10125
|
+
const token = readFileSync3(enrolTokenSourcePath, "utf8").trim();
|
|
9819
10126
|
if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
|
|
9820
10127
|
throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
|
|
9821
10128
|
}
|
|
@@ -9828,7 +10135,7 @@ async function cmdAgent(options, args) {
|
|
|
9828
10135
|
if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
|
|
9829
10136
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
9830
10137
|
}
|
|
9831
|
-
|
|
10138
|
+
writeFileSync3(enrolTokenSourcePath, `${token}
|
|
9832
10139
|
`, { mode: 384, flag: "wx" });
|
|
9833
10140
|
}
|
|
9834
10141
|
}
|
|
@@ -9839,7 +10146,7 @@ async function cmdAgent(options, args) {
|
|
|
9839
10146
|
out.line();
|
|
9840
10147
|
out.line(" Add this machine-specific PUBLIC key as a read-only deploy key:");
|
|
9841
10148
|
out.line();
|
|
9842
|
-
out.line(` ${
|
|
10149
|
+
out.line(` ${readFileSync3(gitPublicKeyPath, "utf8").trim()}`);
|
|
9843
10150
|
out.line();
|
|
9844
10151
|
return 0;
|
|
9845
10152
|
} catch (cause) {
|
|
@@ -9993,6 +10300,86 @@ function cmdProject(options, args) {
|
|
|
9993
10300
|
return 1;
|
|
9994
10301
|
}
|
|
9995
10302
|
}
|
|
10303
|
+
function softwareCoordinates(values) {
|
|
10304
|
+
if (values.length === 0)
|
|
10305
|
+
return;
|
|
10306
|
+
return values.map((coordinate) => {
|
|
10307
|
+
const separator = coordinate.lastIndexOf("@");
|
|
10308
|
+
if (separator < 1 || separator === coordinate.length - 1) {
|
|
10309
|
+
throw new Error(`Software must be <key>@<version>, received: ${coordinate || "<empty>"}.`);
|
|
10310
|
+
}
|
|
10311
|
+
return {
|
|
10312
|
+
id: coordinate.slice(0, separator),
|
|
10313
|
+
version: coordinate.slice(separator + 1)
|
|
10314
|
+
};
|
|
10315
|
+
});
|
|
10316
|
+
}
|
|
10317
|
+
function cmdDeploy(options, args) {
|
|
10318
|
+
const operation = args[0] ?? "check";
|
|
10319
|
+
try {
|
|
10320
|
+
if (options.optionError)
|
|
10321
|
+
throw new Error(options.optionError);
|
|
10322
|
+
if (operation === "init") {
|
|
10323
|
+
const created = initializeDeployFile(options.projectRoot, {
|
|
10324
|
+
name: options.projectName,
|
|
10325
|
+
profile: options.deployProfile,
|
|
10326
|
+
software: softwareCoordinates(options.deploySoftware),
|
|
10327
|
+
requireAttestation: options.requireAttestation,
|
|
10328
|
+
channel: options.deployChannel,
|
|
10329
|
+
force: options.force
|
|
10330
|
+
});
|
|
10331
|
+
if (options.json)
|
|
10332
|
+
out.line(JSON.stringify(created.summary, null, 2));
|
|
10333
|
+
else {
|
|
10334
|
+
out.ok(`Initialized .fz/deploy.json (${created.summary.digest}).`);
|
|
10335
|
+
out.warn("Release and health use safe blockers until you replace them with project-specific commands.");
|
|
10336
|
+
out.step("Run `fz deploy check`, commit the file, then push; the verified Git commit is the live sync.");
|
|
10337
|
+
}
|
|
10338
|
+
return 0;
|
|
10339
|
+
}
|
|
10340
|
+
if (operation === "check" || operation === "sync") {
|
|
10341
|
+
const inspected = inspectDeployFile(options.projectRoot, { channel: options.deployChannel });
|
|
10342
|
+
if (options.json)
|
|
10343
|
+
out.line(JSON.stringify(inspected.summary, null, 2));
|
|
10344
|
+
else {
|
|
10345
|
+
out.step(`${inspected.summary.name} \xB7 v${inspected.summary.version} \xB7 ${inspected.summary.digest}`);
|
|
10346
|
+
out.step(`profiles: ${inspected.summary.profiles.join(", ")}`);
|
|
10347
|
+
for (const problem of inspected.summary.problems)
|
|
10348
|
+
out.fail(problem);
|
|
10349
|
+
if (inspected.summary.ready) {
|
|
10350
|
+
out.ok("Deploy definition is typed, catalog-valid and ready to commit.");
|
|
10351
|
+
if (operation === "sync") {
|
|
10352
|
+
out.step("Commit and push this file. ForgeZero deploys the exact webhook commit; no second live file exists.");
|
|
10353
|
+
}
|
|
10354
|
+
}
|
|
10355
|
+
}
|
|
10356
|
+
return inspected.summary.ready ? 0 : 1;
|
|
10357
|
+
}
|
|
10358
|
+
if (operation === "catalog") {
|
|
10359
|
+
const software = SOFTWARE_CATALOG.filter((entry) => entry.status === "active" || options.deployChannel === "development" && entry.status === "testing");
|
|
10360
|
+
const payload = {
|
|
10361
|
+
channel: options.deployChannel,
|
|
10362
|
+
note: "Only active coordinates are selectable in deploy definitions.",
|
|
10363
|
+
os: OS_CATALOG,
|
|
10364
|
+
software
|
|
10365
|
+
};
|
|
10366
|
+
if (options.json)
|
|
10367
|
+
out.line(JSON.stringify(payload, null, 2));
|
|
10368
|
+
else {
|
|
10369
|
+
out.line(`${options.deployChannel} software catalog (only active coordinates are selectable):`);
|
|
10370
|
+
for (const entry of software) {
|
|
10371
|
+
out.step(`${entry.id}@${entry.version} \xB7 ${entry.os} ${entry.osVersion} ${entry.architecture} \xB7 ${entry.status}`);
|
|
10372
|
+
}
|
|
10373
|
+
}
|
|
10374
|
+
return 0;
|
|
10375
|
+
}
|
|
10376
|
+
out.fail("Usage: fz deploy init|check|sync|catalog [--root <path>] [--channel production|development]");
|
|
10377
|
+
return 2;
|
|
10378
|
+
} catch (cause) {
|
|
10379
|
+
out.fail(cause instanceof Error ? cause.message : String(cause));
|
|
10380
|
+
return 1;
|
|
10381
|
+
}
|
|
10382
|
+
}
|
|
9996
10383
|
function usage() {
|
|
9997
10384
|
out.line(`
|
|
9998
10385
|
fz ${VERSION2} \u2014 ForgeZero control surface
|
|
@@ -10017,6 +10404,10 @@ function usage() {
|
|
|
10017
10404
|
fz project init Create vendor-neutral, Git-persisted AI context
|
|
10018
10405
|
fz project sync Regenerate Claude/Codex/Gemini/Copilot/Cursor adapters
|
|
10019
10406
|
fz project check Fail when truth sources or generated adapters drift
|
|
10407
|
+
fz deploy init Create a typed, fail-safe .fz/deploy.json
|
|
10408
|
+
fz deploy check Validate commands, profiles and software coordinates
|
|
10409
|
+
fz deploy sync Prove readiness and print the Git synchronization rule
|
|
10410
|
+
fz deploy catalog List selectable tested OS/software coordinates
|
|
10020
10411
|
|
|
10021
10412
|
CEREMONY OPTIONS
|
|
10022
10413
|
--key <fp|index> Which agent key to use for custody
|
|
@@ -10039,10 +10430,14 @@ function usage() {
|
|
|
10039
10430
|
--apply Write the unit rather than printing it (root)
|
|
10040
10431
|
--enrol Bind this machine with a one-time token prompted
|
|
10041
10432
|
securely by systemd (tenant-owned compute)
|
|
10042
|
-
--root <path>
|
|
10043
|
-
--name <name>
|
|
10044
|
-
--purpose <text>
|
|
10045
|
-
--
|
|
10433
|
+
--root <path> Project root for project/deploy commands
|
|
10434
|
+
--name <name> Project or deploy name during init
|
|
10435
|
+
--purpose <text> Product outcome during project init
|
|
10436
|
+
--profile <name> Initial deploy profile (default app)
|
|
10437
|
+
--software <key@ver> Initial tested software coordinate; repeatable
|
|
10438
|
+
--channel <name> Catalog view: production or development (shows testing)
|
|
10439
|
+
--attestation Require hardware attestation for every deploy step
|
|
10440
|
+
--force Init may replace an existing generated target
|
|
10046
10441
|
|
|
10047
10442
|
CUSTODY FACTORS
|
|
10048
10443
|
Every custodian share is sealed TWICE and either envelope alone opens it:
|
|
@@ -10074,6 +10469,9 @@ async function runCli() {
|
|
|
10074
10469
|
case "project":
|
|
10075
10470
|
code = cmdProject(options, args);
|
|
10076
10471
|
break;
|
|
10472
|
+
case "deploy":
|
|
10473
|
+
code = cmdDeploy(options, args);
|
|
10474
|
+
break;
|
|
10077
10475
|
case "genesis":
|
|
10078
10476
|
code = await cmdGenesis(options);
|
|
10079
10477
|
break;
|
|
@@ -10097,7 +10495,7 @@ async function runCli() {
|
|
|
10097
10495
|
usage();
|
|
10098
10496
|
code = 1;
|
|
10099
10497
|
}
|
|
10100
|
-
if (args.length > 0 && code === 0 && command !== "agent" && command !== "project") {
|
|
10498
|
+
if (args.length > 0 && code === 0 && command !== "agent" && command !== "project" && command !== "deploy") {
|
|
10101
10499
|
out.warn(`Ignored: ${args.join(" ")}`);
|
|
10102
10500
|
}
|
|
10103
10501
|
process.exit(code);
|
package/dist/provision.js
CHANGED
|
@@ -409,7 +409,7 @@ function observeSoftwareHost(osRelease = readFileSync2("/etc/os-release", "utf8"
|
|
|
409
409
|
architecture
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
|
-
function validateSoftwareRequirements(value,
|
|
412
|
+
function validateSoftwareRequirements(value, _options = {}) {
|
|
413
413
|
if (!Array.isArray(value) || value.length > 32)
|
|
414
414
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
415
415
|
const seen = new Set;
|
|
@@ -428,8 +428,8 @@ function validateSoftwareRequirements(value, options = {}) {
|
|
|
428
428
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
429
429
|
seen.add(requirement.id);
|
|
430
430
|
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
431
|
-
if (!catalog || catalog.status
|
|
432
|
-
throw new Error(`software requirement is not
|
|
431
|
+
if (!catalog || catalog.status !== "active") {
|
|
432
|
+
throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
|
|
433
433
|
}
|
|
434
434
|
return requirement;
|
|
435
435
|
});
|
|
@@ -569,7 +569,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
// src/version.ts
|
|
572
|
-
var VERSION2 = "0.1.
|
|
572
|
+
var VERSION2 = "0.1.31";
|
|
573
573
|
|
|
574
574
|
// src/provision.ts
|
|
575
575
|
function atLeast(version, floor) {
|
package/dist/software-helper.js
CHANGED
|
@@ -51,7 +51,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
|
|
|
51
51
|
architecture
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
function validateSoftwareRequirements(value,
|
|
54
|
+
function validateSoftwareRequirements(value, _options = {}) {
|
|
55
55
|
if (!Array.isArray(value) || value.length > 32)
|
|
56
56
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
57
57
|
const seen = new Set;
|
|
@@ -70,8 +70,8 @@ function validateSoftwareRequirements(value, options = {}) {
|
|
|
70
70
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
71
71
|
seen.add(requirement.id);
|
|
72
72
|
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
73
|
-
if (!catalog || catalog.status
|
|
74
|
-
throw new Error(`software requirement is not
|
|
73
|
+
if (!catalog || catalog.status !== "active") {
|
|
74
|
+
throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
|
|
75
75
|
}
|
|
76
76
|
return requirement;
|
|
77
77
|
});
|
package/dist/software.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export type SoftwareExec = (command: string) => Promise<SoftwareCommandResult>;
|
|
|
36
36
|
export declare const OS_CATALOG: readonly OsCatalogEntry[];
|
|
37
37
|
export declare const SOFTWARE_CATALOG: readonly SoftwareCatalogEntry[];
|
|
38
38
|
export declare function observeSoftwareHost(osRelease?: string, architecture?: NodeJS.Architecture): SoftwareObservation;
|
|
39
|
-
export declare function validateSoftwareRequirements(value: unknown,
|
|
39
|
+
export declare function validateSoftwareRequirements(value: unknown, _options?: {
|
|
40
40
|
channel?: DeploymentChannel;
|
|
41
41
|
}): SoftwareRequirement[];
|
|
42
42
|
export declare function ensureSoftwareRequirements(requirementsInput: unknown, options: {
|
package/dist/software.js
CHANGED
|
@@ -51,7 +51,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
|
|
|
51
51
|
architecture
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
function validateSoftwareRequirements(value,
|
|
54
|
+
function validateSoftwareRequirements(value, _options = {}) {
|
|
55
55
|
if (!Array.isArray(value) || value.length > 32)
|
|
56
56
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
57
57
|
const seen = new Set;
|
|
@@ -70,8 +70,8 @@ function validateSoftwareRequirements(value, options = {}) {
|
|
|
70
70
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
71
71
|
seen.add(requirement.id);
|
|
72
72
|
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
73
|
-
if (!catalog || catalog.status
|
|
74
|
-
throw new Error(`software requirement is not
|
|
73
|
+
if (!catalog || catalog.status !== "active") {
|
|
74
|
+
throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
|
|
75
75
|
}
|
|
76
76
|
return requirement;
|
|
77
77
|
});
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.31";
|