spawnfile 0.1.12 → 0.1.13
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 +9 -2
- package/dist/auth/runtimeCredentials.js +18 -2
- package/dist/cli/devCommands.d.ts +3 -0
- package/dist/cli/devCommands.js +115 -0
- package/dist/cli/lifecycleCommands.js +2 -0
- package/dist/cli/runCli.d.ts +6 -0
- package/dist/cli/runCli.js +4 -0
- package/dist/cli/statusCommand.d.ts +2 -1
- package/dist/cli/statusCommand.js +50 -16
- package/dist/compiler/buildProject.d.ts +9 -1
- package/dist/compiler/buildProject.js +44 -5
- package/dist/compiler/compileProject.d.ts +2 -0
- package/dist/compiler/compileProject.js +3 -1
- package/dist/compiler/containerArtifactsRender.js +25 -69
- package/dist/compiler/containerEntrypointRender.js +4 -1
- package/dist/compiler/containerPackageRender.d.ts +15 -0
- package/dist/compiler/containerPackageRender.js +63 -0
- package/dist/compiler/containerTargetResources.js +3 -1
- package/dist/compiler/moltnetArtifacts.js +16 -1
- package/dist/compiler/moltnetBinaries.d.ts +4 -1
- package/dist/compiler/moltnetBinaries.js +24 -3
- package/dist/compiler/moltnetClientConfig.js +11 -0
- package/dist/compiler/moltnetRuntimeConfig.js +11 -0
- package/dist/compiler/runProject.js +7 -1
- package/dist/compiler/runProjectDocker.js +157 -12
- package/dist/compiler/upProject.js +1 -0
- package/dist/deployment/dockerRecover.d.ts +17 -0
- package/dist/deployment/dockerRecover.js +129 -0
- package/dist/deployment/index.d.ts +1 -0
- package/dist/deployment/index.js +1 -0
- package/dist/dev/activity.d.ts +14 -0
- package/dist/dev/activity.js +48 -0
- package/dist/dev/docker.d.ts +16 -0
- package/dist/dev/docker.js +71 -0
- package/dist/dev/index.d.ts +2 -0
- package/dist/dev/index.js +2 -0
- package/dist/dev/project.d.ts +35 -0
- package/dist/dev/project.js +164 -0
- package/dist/runtime/common.d.ts +2 -0
- package/dist/runtime/common.js +1 -1
- package/dist/runtime/container.d.ts +1 -0
- package/dist/runtime/container.js +93 -10
- package/dist/runtime/install.d.ts +1 -1
- package/dist/runtime/install.js +43 -1
- package/dist/runtime/pi/adapter.d.ts +4 -0
- package/dist/runtime/pi/adapter.js +198 -0
- package/dist/runtime/pi/appActivitySource.d.ts +1 -0
- package/dist/runtime/pi/appActivitySource.js +87 -0
- package/dist/runtime/pi/appCoreSource.d.ts +1 -0
- package/dist/runtime/pi/appCoreSource.js +354 -0
- package/dist/runtime/pi/appPreludeSource.d.ts +1 -0
- package/dist/runtime/pi/appPreludeSource.js +103 -0
- package/dist/runtime/pi/appSource.d.ts +1 -0
- package/dist/runtime/pi/appSource.js +8 -0
- package/dist/runtime/pi/appTemplate.d.ts +28 -0
- package/dist/runtime/pi/appTemplate.js +144 -0
- package/dist/runtime/pi/runAuth.d.ts +2 -0
- package/dist/runtime/pi/runAuth.js +68 -0
- package/dist/runtime/registry.js +3 -0
- package/dist/runtime/types.d.ts +1 -0
- package/dist/status/deployments.js +5 -12
- package/package.json +7 -2
- package/runtimes.yaml +27 -11
package/README.md
CHANGED
|
@@ -40,14 +40,20 @@ spawnfile compile # lower to runtime-native outpu
|
|
|
40
40
|
spawnfile status . # read declared/compiled status
|
|
41
41
|
spawnfile auth sync --profile dev --env-file .env
|
|
42
42
|
spawnfile build --tag my-agent # compile + docker build
|
|
43
|
+
spawnfile up . --context gpu-4090 --detach # build and run on a Docker context
|
|
44
|
+
spawnfile dev up . --auth-profile dev # detached dev loop in .spawn-dev
|
|
45
|
+
spawnfile dev apply . --agent researcher # hot-add/reload one Pi agent
|
|
46
|
+
spawnfile dev activity . --agent researcher # inspect buffered Pi activity events
|
|
43
47
|
spawnfile run --tag my-agent --auth-profile dev --detach
|
|
44
48
|
spawnfile status . --live # inspect the detached deployment
|
|
45
49
|
spawnfile publish . --tag you/my-agent:1.0.0 # compile + build + verify + push
|
|
46
50
|
```
|
|
47
51
|
|
|
48
|
-
Compiled output lands under `.spawn/` by default, including a `Dockerfile`, `entrypoint.sh`, `.env.example`, and a prebuilt `container/rootfs/` tree. `spawnfile build` uses the pinned runtime artifacts from `runtimes.yaml`; it does not rebuild runtimes from source.
|
|
52
|
+
Compiled output lands under `.spawn/` by default, including a `Dockerfile`, `entrypoint.sh`, `.env.example`, and a prebuilt `container/rootfs/` tree. `spawnfile build` uses the pinned runtime artifacts from `runtimes.yaml`; it does not rebuild runtimes from source. Daimon, OpenClaw, and PicoClaw use published copyable artifact images by default, so normal prompt/config edits reuse their dependency layers. To test a local runtime artifact instead, set `SPAWNFILE_DAIMON_RUNTIME_IMAGE`, `SPAWNFILE_OPENCLAW_RUNTIME_IMAGE`, or `SPAWNFILE_PICOCLAW_RUNTIME_IMAGE` to a local image tag. For Pi-heavy orgs, build a reusable runtime base with `npm run runtime:pi-base -- noopolis/spawnfile-pi-runtime:0.79.9-node24`, then set `SPAWNFILE_PI_RUNTIME_BASE_IMAGE` during `spawnfile up`. For `build`/`up` on a docker `--context`, Moltnet release assets are staged for that context's architecture (`amd64` or `arm64`); for local-only manual compile targeting a fixed architecture, set `SPAWNFILE_MOLTNET_TARGET_ARCH=amd64|arm64`.
|
|
49
53
|
|
|
50
|
-
`spawnfile status` is read-only. By default it shows authored and compiled state without Docker, runtime, or Moltnet calls. With `--live`, it reads the selected detached deployment record, inspects the recorded Docker target, runs adapter-owned runtime probes, and checks Moltnet metadata without reading message bodies. Add `--logs` for a redacted Docker log tail, or `--watch` to refresh status continuously.
|
|
54
|
+
`spawnfile status` is read-only. By default it shows authored and compiled state without Docker, runtime, or Moltnet calls. With `--live`, it reads the selected detached deployment record, inspects the recorded Docker target, runs adapter-owned runtime probes, and checks Moltnet metadata without reading message bodies. Add `--logs` for a redacted Docker log tail, or `--watch` to refresh status continuously. For a remote Docker context where the local record is missing, pass `--context <name>` with `--live` to recover the deployment from Spawnfile container labels.
|
|
55
|
+
|
|
56
|
+
`spawnfile dev` is the source-backed interactive loop. It uses `.spawn-dev/` by default, starts a detached dev deployment with `spawnfile dev up`, and can hot-apply one Daimon runtime agent with `spawnfile dev apply --agent <id>` without restarting the rest of the org. Hot apply recompiles source, copies the selected agent workspace, Daimon config, matching Moltnet node configs, and managed Moltnet server configs into the running container, loads it through the Daimon control endpoint, and starts only that agent's Moltnet bridges when it is new. `spawnfile dev activity` reads the generated Daimon app's bounded activity buffer as JSON lines so operators can see queued wakes, turn starts/completions, runtime event types, output completions, and errors without mixing those diagnostics into Moltnet chat. Running managed Moltnet servers keep their current in-memory room membership until an operator-token `moltnet apply` or server restart reconciles the copied server config.
|
|
51
57
|
|
|
52
58
|
Compiled images are self-describing: `spawnfile publish` pushes one to any OCI registry, and anyone can run it with no source — `spawnfile up you/my-agent:1.0.0 --deployment prod --detach --auth-profile me` — or inspect what it needs first with `spawnfile status you/my-agent:1.0.0`. See [`specs/DISTRIBUTION.md`](specs/DISTRIBUTION.md).
|
|
53
59
|
|
|
@@ -98,6 +104,7 @@ v0.1 targets autonomous agent runtimes that share a markdown workspace identity
|
|
|
98
104
|
|-----------|---------------|---------|-----------------------------------------------|
|
|
99
105
|
| OpenClaw | active | ✅ | Discord, Telegram, WhatsApp, Slack |
|
|
100
106
|
| PicoClaw | active | | Discord, Telegram, Slack (WhatsApp blocked) |
|
|
107
|
+
| Pi | active | | Embedded org app, Moltnet client config |
|
|
101
108
|
| NullClaw | exploratory | | No active adapter yet |
|
|
102
109
|
| ZeroClaw | exploratory | | No active adapter yet |
|
|
103
110
|
| OpenFang | exploratory | | No active adapter yet |
|
|
@@ -14,6 +14,21 @@ const parseJson = (filePath, content) => {
|
|
|
14
14
|
throw new SpawnfileError("validation_error", `Invalid imported auth file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
|
+
const decodeJwtExpiry = (token) => {
|
|
18
|
+
const payload = token.split(".")[1];
|
|
19
|
+
if (!payload) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
24
|
+
return typeof decoded.exp === "number" && Number.isFinite(decoded.exp)
|
|
25
|
+
? decoded.exp * 1000
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
17
32
|
export const loadImportedClaudeCodeCredential = async (sourceDirectory) => {
|
|
18
33
|
const credentialsPath = path.join(sourceDirectory, ".credentials.json");
|
|
19
34
|
if (!(await fileExists(credentialsPath))) {
|
|
@@ -66,11 +81,12 @@ export const loadImportedCodexCredential = async (sourceDirectory) => {
|
|
|
66
81
|
refresh.length === 0) {
|
|
67
82
|
return null;
|
|
68
83
|
}
|
|
69
|
-
const
|
|
84
|
+
const jwtExpiry = decodeJwtExpiry(access);
|
|
85
|
+
const expires = jwtExpiry ?? (await stat(authPath)).mtimeMs + 60 * 60 * 1000;
|
|
70
86
|
return {
|
|
71
87
|
access,
|
|
72
88
|
...(typeof accountId === "string" && accountId.length > 0 ? { accountId } : {}),
|
|
73
|
-
expires
|
|
89
|
+
expires,
|
|
74
90
|
refresh
|
|
75
91
|
};
|
|
76
92
|
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { InvalidArgumentError } from "commander";
|
|
2
|
+
const parsePositiveIntegerOption = (value) => {
|
|
3
|
+
if (!/^[1-9]\d*$/u.test(value)) {
|
|
4
|
+
throw new InvalidArgumentError("must be a positive integer");
|
|
5
|
+
}
|
|
6
|
+
return Number(value);
|
|
7
|
+
};
|
|
8
|
+
export const registerDevCommands = (program, handlers, streams) => {
|
|
9
|
+
const dev = program.command("dev").description("Run an interactive Spawnfile development deployment");
|
|
10
|
+
dev
|
|
11
|
+
.command("up")
|
|
12
|
+
.description("Compile, build, and start a detached dev deployment under .spawn-dev")
|
|
13
|
+
.argument("[path]", "Project directory or Spawnfile path", process.cwd())
|
|
14
|
+
.option("--auth-profile <name>", "Local Spawnfile auth profile")
|
|
15
|
+
.option("--context <name>", "Docker context for the deployment target")
|
|
16
|
+
.option("--deployment <name>", "Detached deployment record name")
|
|
17
|
+
.option("--docker-command <command>", "Docker command")
|
|
18
|
+
.option("--env-file <file>", "Path to an env file for runtime secrets")
|
|
19
|
+
.option("--name <container>", "Docker container name")
|
|
20
|
+
.option("-o, --out <directory>", "Dev output directory")
|
|
21
|
+
.option("-t, --tag <image>", "Docker image tag")
|
|
22
|
+
.action(async (inputPath, options) => {
|
|
23
|
+
const result = await handlers.devUpProject(inputPath, {
|
|
24
|
+
authProfile: options.authProfile,
|
|
25
|
+
containerName: options.name,
|
|
26
|
+
deploymentName: options.deployment,
|
|
27
|
+
dockerCommand: options.dockerCommand,
|
|
28
|
+
dockerContext: options.context,
|
|
29
|
+
envFilePath: options.envFile,
|
|
30
|
+
imageTag: options.tag,
|
|
31
|
+
outputDirectory: options.out
|
|
32
|
+
});
|
|
33
|
+
streams.stdout(`dev deployment: ${result.containerName ?? "unknown"}`);
|
|
34
|
+
streams.stdout(`image: ${result.imageTag}`);
|
|
35
|
+
streams.stdout(`compiled to ${result.outputDirectory}`);
|
|
36
|
+
if (result.deploymentRecordPath) {
|
|
37
|
+
streams.stdout(`record: ${result.deploymentRecordPath}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
dev
|
|
41
|
+
.command("apply")
|
|
42
|
+
.description("Hot-apply one agent into a running dev deployment")
|
|
43
|
+
.argument("[path]", "Project directory or Spawnfile path", process.cwd())
|
|
44
|
+
.requiredOption("--agent <id>", "Agent id, slug, or name to hot-apply")
|
|
45
|
+
.option("--deployment <name>", "Detached deployment record name")
|
|
46
|
+
.option("--docker-command <command>", "Docker command")
|
|
47
|
+
.option("-o, --out <directory>", "Dev output directory")
|
|
48
|
+
.action(async (inputPath, options) => {
|
|
49
|
+
const result = await handlers.devApplyProject(inputPath, {
|
|
50
|
+
agent: options.agent,
|
|
51
|
+
deploymentName: options.deployment,
|
|
52
|
+
dockerCommand: options.dockerCommand,
|
|
53
|
+
outputDirectory: options.out
|
|
54
|
+
});
|
|
55
|
+
streams.stdout(`applied agent ${result.agentId}`);
|
|
56
|
+
streams.stdout(`container: ${result.containerName}`);
|
|
57
|
+
streams.stdout(`bridge: ${result.bridgeStarted ? "started" : "unchanged"}`);
|
|
58
|
+
});
|
|
59
|
+
dev
|
|
60
|
+
.command("restart")
|
|
61
|
+
.description("Reload one agent in a running dev deployment")
|
|
62
|
+
.argument("[path]", "Project directory or Spawnfile path", process.cwd())
|
|
63
|
+
.requiredOption("--agent <id>", "Agent id, slug, or name to reload")
|
|
64
|
+
.option("--deployment <name>", "Detached deployment record name")
|
|
65
|
+
.option("--docker-command <command>", "Docker command")
|
|
66
|
+
.option("-o, --out <directory>", "Dev output directory")
|
|
67
|
+
.action(async (inputPath, options) => {
|
|
68
|
+
const result = await handlers.devRestartProject(inputPath, {
|
|
69
|
+
agent: options.agent,
|
|
70
|
+
deploymentName: options.deployment,
|
|
71
|
+
dockerCommand: options.dockerCommand,
|
|
72
|
+
outputDirectory: options.out
|
|
73
|
+
});
|
|
74
|
+
streams.stdout(`restarted agent ${result.agentId}`);
|
|
75
|
+
streams.stdout(`container: ${result.containerName}`);
|
|
76
|
+
streams.stdout(`bridge: ${result.bridgeStarted ? "started" : "unchanged"}`);
|
|
77
|
+
});
|
|
78
|
+
dev
|
|
79
|
+
.command("activity")
|
|
80
|
+
.description("Print the current Pi agent activity buffer from a running dev deployment")
|
|
81
|
+
.argument("[path]", "Project directory or Spawnfile path", process.cwd())
|
|
82
|
+
.option("--agent <id>", "Only show events for one agent id, slug, or name")
|
|
83
|
+
.option("--deployment <name>", "Detached deployment record name")
|
|
84
|
+
.option("--docker-command <command>", "Docker command")
|
|
85
|
+
.option("-o, --out <directory>", "Dev output directory")
|
|
86
|
+
.option("--tail <count>", "Maximum buffered events to print", parsePositiveIntegerOption)
|
|
87
|
+
.action(async (inputPath, options) => {
|
|
88
|
+
const result = await handlers.devActivityProject(inputPath, {
|
|
89
|
+
agent: options.agent,
|
|
90
|
+
deploymentName: options.deployment,
|
|
91
|
+
dockerCommand: options.dockerCommand,
|
|
92
|
+
outputDirectory: options.out,
|
|
93
|
+
tail: options.tail
|
|
94
|
+
});
|
|
95
|
+
for (const event of result.events) {
|
|
96
|
+
streams.stdout(JSON.stringify(event));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
dev
|
|
100
|
+
.command("stop")
|
|
101
|
+
.description("Stop the recorded dev deployment container")
|
|
102
|
+
.argument("[path]", "Project directory or Spawnfile path", process.cwd())
|
|
103
|
+
.option("--deployment <name>", "Detached deployment record name")
|
|
104
|
+
.option("--docker-command <command>", "Docker command")
|
|
105
|
+
.option("-o, --out <directory>", "Dev output directory")
|
|
106
|
+
.action(async (inputPath, options) => {
|
|
107
|
+
const result = await handlers.devStopProject(inputPath, {
|
|
108
|
+
deploymentName: options.deployment,
|
|
109
|
+
dockerCommand: options.dockerCommand,
|
|
110
|
+
outputDirectory: options.out
|
|
111
|
+
});
|
|
112
|
+
streams.stdout(`stopped dev deployment ${result.deploymentName}`);
|
|
113
|
+
streams.stdout(`container: ${result.containerName}`);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
@@ -33,11 +33,13 @@ export const registerLifecycleCommands = (program, handlers, streams) => {
|
|
|
33
33
|
.command("build")
|
|
34
34
|
.description("Compile and build the organization Docker image")
|
|
35
35
|
.argument("[path]", "Project directory or Spawnfile path", process.cwd())
|
|
36
|
+
.option("--context <name>", "Docker context for the build target")
|
|
36
37
|
.option("--docker-command <command>", "Docker command")
|
|
37
38
|
.option("-o, --out <directory>", "Output directory")
|
|
38
39
|
.option("-t, --tag <image>", "Docker image tag")
|
|
39
40
|
.action(async (inputPath, options) => {
|
|
40
41
|
const result = await handlers.buildProject(inputPath, {
|
|
42
|
+
dockerContext: options.context,
|
|
41
43
|
dockerCommand: options.dockerCommand,
|
|
42
44
|
imageTag: options.tag,
|
|
43
45
|
outputDirectory: options.out
|
package/dist/cli/runCli.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
|
|
2
2
|
import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, publishProject, upProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
|
|
3
|
+
import { devActivityProject, devApplyProject, devRestartProject, devStopProject, devUpProject } from "../dev/index.js";
|
|
3
4
|
import { consumeImageUp } from "../distribution/index.js";
|
|
4
5
|
import { listRuntimeAdapters } from "../runtime/index.js";
|
|
5
6
|
export interface CliStreams {
|
|
@@ -33,6 +34,11 @@ export interface CliHandlers {
|
|
|
33
34
|
runProject: typeof runProject;
|
|
34
35
|
upProject: typeof upProject;
|
|
35
36
|
consumeImageUp: typeof consumeImageUp;
|
|
37
|
+
devActivityProject: typeof devActivityProject;
|
|
38
|
+
devApplyProject: typeof devApplyProject;
|
|
39
|
+
devRestartProject: typeof devRestartProject;
|
|
40
|
+
devStopProject: typeof devStopProject;
|
|
41
|
+
devUpProject: typeof devUpProject;
|
|
36
42
|
setProjectPrimaryModel: typeof setProjectPrimaryModel;
|
|
37
43
|
setProjectRuntime: typeof setProjectRuntime;
|
|
38
44
|
setProjectSurfaceAccess: typeof setProjectSurfaceAccess;
|
package/dist/cli/runCli.js
CHANGED
|
@@ -2,9 +2,11 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
|
|
4
4
|
import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, publishProject, upProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
|
|
5
|
+
import { devActivityProject, devApplyProject, devRestartProject, devStopProject, devUpProject } from "../dev/index.js";
|
|
5
6
|
import { consumeImageUp } from "../distribution/index.js";
|
|
6
7
|
import { errorExitCode, isSpawnfileError } from "../shared/index.js";
|
|
7
8
|
import { listRuntimeAdapters } from "../runtime/index.js";
|
|
9
|
+
import { registerDevCommands } from "./devCommands.js";
|
|
8
10
|
import { registerLifecycleCommands } from "./lifecycleCommands.js";
|
|
9
11
|
import { registerModelCommands } from "./modelCommands.js";
|
|
10
12
|
import { registerRuntimeCommands } from "./runtimeCommands.js";
|
|
@@ -32,6 +34,7 @@ const createDefaultHandlers = () => ({
|
|
|
32
34
|
importClaudeCodeAuth, importCodexAuth, importEnvFile,
|
|
33
35
|
initProject, listRuntimeAdapters, removeProjectSurface, requireAuthProfile,
|
|
34
36
|
runProject, setProjectPrimaryModel, setProjectRuntime, upProject, consumeImageUp,
|
|
37
|
+
devActivityProject, devApplyProject, devRestartProject, devStopProject, devUpProject,
|
|
35
38
|
setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth
|
|
36
39
|
});
|
|
37
40
|
const isCliStreams = (value) => {
|
|
@@ -104,6 +107,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
|
|
|
104
107
|
writeOut: (message) => writeCommanderOutput(streams.stdout, message)
|
|
105
108
|
});
|
|
106
109
|
registerLifecycleCommands(program, handlers, streams);
|
|
110
|
+
registerDevCommands(program, handlers, streams);
|
|
107
111
|
program
|
|
108
112
|
.command("init")
|
|
109
113
|
.description("Scaffold a new Spawnfile project or team in a directory")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { inspectDockerDeployment } from "../deployment/index.js";
|
|
2
|
+
import { inspectDockerDeployment, recoverDockerDeploymentRecords } from "../deployment/index.js";
|
|
3
3
|
import { collectDeploymentLogObservations, collectMoltnetProbeObservations, collectRuntimeProbeObservations, type StatusCommandResult, type StatusExitCode } from "../status/index.js";
|
|
4
4
|
import type { CliHandlers, CliStreams } from "./runCli.js";
|
|
5
5
|
type StatusCommandHandlers = Pick<CliHandlers, "buildOrganizationView"> & Partial<Pick<CliHandlers, "requireAuthProfile">>;
|
|
@@ -8,6 +8,7 @@ type StatusCommandLiveHandlers = {
|
|
|
8
8
|
collectMoltnetProbeObservations?: typeof collectMoltnetProbeObservations;
|
|
9
9
|
collectRuntimeProbeObservations?: typeof collectRuntimeProbeObservations;
|
|
10
10
|
inspectDockerDeployment?: typeof inspectDockerDeployment;
|
|
11
|
+
recoverDockerDeploymentRecords?: typeof recoverDockerDeploymentRecords;
|
|
11
12
|
};
|
|
12
13
|
type StatusCommandHandlersWithLive = StatusCommandHandlers & StatusCommandLiveHandlers;
|
|
13
14
|
export interface StatusCommandOptions {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { inspectDockerDeployment, listDeploymentRecords, listHomeDeploymentRecords, readHomeDeploymentRecord, readHomeDeploymentReport } from "../deployment/index.js";
|
|
2
|
-
import { extractImageReport, parseDistributionReport, projectImageOrganizationView, renderImageInterface } from "../distribution/index.js";
|
|
1
|
+
import { inspectDockerDeployment, listDeploymentRecords, listHomeDeploymentRecords, readHomeDeploymentRecord, readHomeDeploymentReport, recoverDockerDeploymentRecords } from "../deployment/index.js";
|
|
2
|
+
import { extractImageReport, normalizeProjectLabelSlug, parseDistributionReport, projectImageOrganizationView, renderImageInterface } from "../distribution/index.js";
|
|
3
3
|
import { collectRegistryDriftObservations } from "../status/index.js";
|
|
4
4
|
import { resolveProjectOutputDirectory } from "../filesystem/index.js";
|
|
5
5
|
import { DEFAULT_OUTPUT_DIRECTORY, errorExitCode, SpawnfileError } from "../shared/index.js";
|
|
6
6
|
import { resolveCommandInput } from "./resolveCommandInput.js";
|
|
7
7
|
import { loadedImageCompileReport } from "../status/index.js";
|
|
8
|
-
import { createStaticStatus, createDeploymentSummaries, collectDeploymentLogObservations, collectMoltnetProbeObservations, collectRuntimeProbeObservations, exitCodeForStatus, loadCompileReport, renderStatus, resolveStatusSelector } from "../status/index.js";
|
|
8
|
+
import { createStaticStatus, createDeploymentSummaries, collectDeploymentLogObservations, collectMoltnetProbeObservations, collectRuntimeProbeObservations, exitCodeForStatus, flattenOrganizationNodes, loadCompileReport, renderStatus, resolveStatusSelector } from "../status/index.js";
|
|
9
9
|
const inputFailure = (message) => ({
|
|
10
10
|
error: message,
|
|
11
11
|
exitCode: 2
|
|
@@ -56,16 +56,36 @@ const resolveDeploymentRecords = (records, options) => {
|
|
|
56
56
|
return records;
|
|
57
57
|
};
|
|
58
58
|
const inspectDeployments = async (records, handlers, options, timeoutMs) => {
|
|
59
|
-
if (!options.live
|
|
59
|
+
if (!options.live) {
|
|
60
60
|
return new Map();
|
|
61
61
|
}
|
|
62
62
|
const inspect = handlers.inspectDockerDeployment ?? inspectDockerDeployment;
|
|
63
63
|
const inspections = await Promise.all(records.map(async ({ record }) => [
|
|
64
64
|
record.name,
|
|
65
|
-
await inspect(record, {
|
|
65
|
+
await inspect(record, {
|
|
66
|
+
dockerCommand: options.dockerCommand,
|
|
67
|
+
timeoutMs
|
|
68
|
+
})
|
|
66
69
|
]));
|
|
67
70
|
return new Map(inspections);
|
|
68
71
|
};
|
|
72
|
+
const containsFromView = (view) => flattenOrganizationNodes(view).map((node) => ({ id: node.id, kind: node.kind }));
|
|
73
|
+
const runtimeInstanceIdsFromReport = (loadedReport) => loadedReport.kind === "loaded"
|
|
74
|
+
? loadedReport.report.runtimeInstances.map((instance) => instance.id).sort()
|
|
75
|
+
: [];
|
|
76
|
+
const recoverContextDeployments = async (input) => {
|
|
77
|
+
const recover = input.handlers.recoverDockerDeploymentRecords ?? recoverDockerDeploymentRecords;
|
|
78
|
+
return recover({
|
|
79
|
+
contains: containsFromView(input.view),
|
|
80
|
+
context: input.options.context,
|
|
81
|
+
dockerCommand: input.options.dockerCommand,
|
|
82
|
+
outputDirectory: input.outputDirectory,
|
|
83
|
+
projectLabel: input.projectLabel,
|
|
84
|
+
runtimeInstanceIds: runtimeInstanceIdsFromReport(input.loadedReport),
|
|
85
|
+
sourceRoot: input.sourceRoot,
|
|
86
|
+
timeoutMs: input.timeoutMs
|
|
87
|
+
});
|
|
88
|
+
};
|
|
69
89
|
const resolveAuthValues = async (records, handlers) => {
|
|
70
90
|
const values = {};
|
|
71
91
|
if (!handlers.requireAuthProfile) {
|
|
@@ -204,7 +224,12 @@ const runHomeDeploymentStatus = async (options, handlers, mode, timeoutMs) => {
|
|
|
204
224
|
timeoutMs
|
|
205
225
|
}),
|
|
206
226
|
...(options.logs
|
|
207
|
-
? await collectDeploymentLogs({
|
|
227
|
+
? await collectDeploymentLogs({
|
|
228
|
+
deployments: recordValues,
|
|
229
|
+
dockerCommand: options.dockerCommand,
|
|
230
|
+
loadedReport,
|
|
231
|
+
timeoutMs
|
|
232
|
+
})
|
|
208
233
|
: []),
|
|
209
234
|
...(options.pullCheck
|
|
210
235
|
? await collectRegistryDriftObservations({
|
|
@@ -250,22 +275,19 @@ export const executeStatusCommand = async (inputPath, options, handlers) => {
|
|
|
250
275
|
if (commandInput.kind === "image" && !options.deployment) {
|
|
251
276
|
return runStaticImageStatus(commandInput.ref, options, mode === "json");
|
|
252
277
|
}
|
|
253
|
-
if (options.deployment && (commandInput.kind === "image" || usedDefaultPath)) {
|
|
278
|
+
if (options.deployment && !options.context && (commandInput.kind === "image" || usedDefaultPath)) {
|
|
254
279
|
const timeoutForHome = resolveTimeoutMs(options);
|
|
255
280
|
if (typeof timeoutForHome !== "number" && timeoutForHome !== undefined) {
|
|
256
281
|
return timeoutForHome;
|
|
257
282
|
}
|
|
258
283
|
return runHomeDeploymentStatus(options, handlers, mode, timeoutForHome);
|
|
259
284
|
}
|
|
260
|
-
if (options.context && !options.
|
|
261
|
-
return inputFailure("status
|
|
285
|
+
if (options.context && !options.live) {
|
|
286
|
+
return inputFailure("status --context requires --live");
|
|
262
287
|
}
|
|
263
288
|
if (options.logs && !options.live) {
|
|
264
289
|
return inputFailure("status --logs requires --live");
|
|
265
290
|
}
|
|
266
|
-
if (options.logs && options.recover) {
|
|
267
|
-
return inputFailure("status --logs is not available with --recover");
|
|
268
|
-
}
|
|
269
291
|
if (options.recover && !options.context) {
|
|
270
292
|
return inputFailure("status --recover requires --context");
|
|
271
293
|
}
|
|
@@ -289,12 +311,23 @@ export const executeStatusCommand = async (inputPath, options, handlers) => {
|
|
|
289
311
|
}
|
|
290
312
|
let deploymentRecords;
|
|
291
313
|
try {
|
|
292
|
-
deploymentRecords =
|
|
314
|
+
deploymentRecords = options.context
|
|
315
|
+
? await recoverContextDeployments({
|
|
316
|
+
handlers,
|
|
317
|
+
loadedReport,
|
|
318
|
+
options,
|
|
319
|
+
outputDirectory,
|
|
320
|
+
projectLabel: normalizeProjectLabelSlug(view.root.name),
|
|
321
|
+
sourceRoot: view.root.source,
|
|
322
|
+
timeoutMs,
|
|
323
|
+
view
|
|
324
|
+
})
|
|
325
|
+
: await listDeploymentRecords(outputDirectory);
|
|
293
326
|
}
|
|
294
327
|
catch (error) {
|
|
295
328
|
return {
|
|
296
329
|
error: error instanceof Error ? error.message : String(error),
|
|
297
|
-
exitCode:
|
|
330
|
+
exitCode: errorExitCode(error)
|
|
298
331
|
};
|
|
299
332
|
}
|
|
300
333
|
const selectedDeploymentRecords = resolveDeploymentRecords(deploymentRecords, options);
|
|
@@ -327,6 +360,7 @@ export const executeStatusCommand = async (inputPath, options, handlers) => {
|
|
|
327
360
|
...(options.logs
|
|
328
361
|
? await collectDeploymentLogs({
|
|
329
362
|
deployments: selectedDeploymentRecordValues,
|
|
363
|
+
dockerCommand: options.dockerCommand,
|
|
330
364
|
loadedReport,
|
|
331
365
|
timeoutMs
|
|
332
366
|
})
|
|
@@ -340,7 +374,7 @@ export const executeStatusCommand = async (inputPath, options, handlers) => {
|
|
|
340
374
|
context: options.context ?? null,
|
|
341
375
|
deploymentName: options.deployment ?? null,
|
|
342
376
|
logs: options.logs ?? false,
|
|
343
|
-
recover: options.recover
|
|
377
|
+
recover: options.recover === true || Boolean(options.context),
|
|
344
378
|
requested: options.live ?? false
|
|
345
379
|
},
|
|
346
380
|
liveObservations,
|
|
@@ -368,7 +402,7 @@ export const registerStatusCommand = (program, handlers, streams, setExitCode) =
|
|
|
368
402
|
.option("--pull", "Pull the image before inspecting (image references only)")
|
|
369
403
|
.option("--pull-check", "Check the registry for a newer image digest (networked)")
|
|
370
404
|
.option("--docker-command <command>", "Docker command")
|
|
371
|
-
.option("--context <name>", "Docker context for
|
|
405
|
+
.option("--context <name>", "Docker context for live remote deployment recovery")
|
|
372
406
|
.option("--recover", "Recover live status from labels instead of a deployment record")
|
|
373
407
|
.option("--logs", "Include redacted logs when supported")
|
|
374
408
|
.option("--timeout <ms>", "Bound live Docker/runtime checks in milliseconds")
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { type CompileProjectOptions, type CompileProjectResult } from "./compileProject.js";
|
|
2
|
+
import type { MoltnetTargetArchitecture } from "./moltnetBinaries.js";
|
|
2
3
|
export interface DockerBuildInvocation {
|
|
3
4
|
args: string[];
|
|
4
5
|
command: string;
|
|
5
6
|
cwd: string;
|
|
7
|
+
dockerContext?: string | null;
|
|
6
8
|
imageTag: string;
|
|
7
9
|
}
|
|
8
10
|
export type DockerBuildRunner = (invocation: DockerBuildInvocation) => Promise<void>;
|
|
9
11
|
export interface BuildProjectOptions extends CompileProjectOptions {
|
|
10
12
|
buildRunner?: DockerBuildRunner;
|
|
11
13
|
dockerCommand?: string;
|
|
14
|
+
dockerContext?: string;
|
|
12
15
|
imageTag?: string;
|
|
13
16
|
}
|
|
14
17
|
export interface BuildProjectResult extends CompileProjectResult {
|
|
15
18
|
imageTag: string;
|
|
16
19
|
}
|
|
17
20
|
export declare const createDefaultImageTag: (projectRoot: string) => string;
|
|
18
|
-
export declare const createDockerBuildInvocation: (outputDirectory: string, imageTag: string,
|
|
21
|
+
export declare const createDockerBuildInvocation: (outputDirectory: string, imageTag: string, options?: string | {
|
|
22
|
+
dockerCommand?: string;
|
|
23
|
+
dockerContext?: string;
|
|
24
|
+
}) => DockerBuildInvocation;
|
|
25
|
+
export declare const normalizeDockerArchitecture: (architecture: string) => MoltnetTargetArchitecture;
|
|
26
|
+
export declare const resolveDockerBuildArchitecture: (options: Pick<BuildProjectOptions, "dockerCommand" | "dockerContext">) => Promise<MoltnetTargetArchitecture | undefined>;
|
|
19
27
|
export declare const runDockerBuild: DockerBuildRunner;
|
|
20
28
|
export declare const buildProject: (inputPath: string, options?: BuildProjectOptions) => Promise<BuildProjectResult>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
3
4
|
import { SpawnfileError } from "../shared/index.js";
|
|
4
5
|
import { compileProject } from "./compileProject.js";
|
|
5
6
|
import { slugify } from "./helpers.js";
|
|
7
|
+
const execFile = promisify(execFileCallback);
|
|
6
8
|
export const createDefaultImageTag = (projectRoot) => {
|
|
7
9
|
const baseName = slugify(path.basename(projectRoot));
|
|
8
10
|
return `spawnfile-${baseName || "project"}`;
|
|
@@ -13,12 +15,44 @@ const resolveImageTagRoot = (inputPath) => {
|
|
|
13
15
|
? path.dirname(resolvedPath)
|
|
14
16
|
: resolvedPath;
|
|
15
17
|
};
|
|
16
|
-
export const createDockerBuildInvocation = (outputDirectory, imageTag,
|
|
17
|
-
args:
|
|
18
|
-
|
|
18
|
+
export const createDockerBuildInvocation = (outputDirectory, imageTag, options = "docker") => ({
|
|
19
|
+
args: typeof options === "string" || !options.dockerContext
|
|
20
|
+
? ["build", "-t", imageTag, "."]
|
|
21
|
+
: ["--context", options.dockerContext, "build", "-t", imageTag, "."],
|
|
22
|
+
command: typeof options === "string" ? options : options.dockerCommand ?? "docker",
|
|
19
23
|
cwd: outputDirectory,
|
|
24
|
+
dockerContext: typeof options === "string" ? null : options.dockerContext ?? null,
|
|
20
25
|
imageTag
|
|
21
26
|
});
|
|
27
|
+
export const normalizeDockerArchitecture = (architecture) => {
|
|
28
|
+
switch (architecture.trim()) {
|
|
29
|
+
case "amd64":
|
|
30
|
+
case "x64":
|
|
31
|
+
case "x86_64":
|
|
32
|
+
return "amd64";
|
|
33
|
+
case "aarch64":
|
|
34
|
+
case "arm64":
|
|
35
|
+
return "arm64";
|
|
36
|
+
default:
|
|
37
|
+
throw new SpawnfileError("compile_error", `Docker target architecture ${architecture.trim() || "unknown"} is not supported by staged Moltnet binaries`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const resolveDockerBuildArchitecture = async (options) => {
|
|
41
|
+
if (!options.dockerContext) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execFile(options.dockerCommand ?? "docker", ["--context", options.dockerContext, "info", "--format", "{{.Architecture}}"], { timeout: 10_000 });
|
|
46
|
+
return normalizeDockerArchitecture(stdout);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof SpawnfileError) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
53
|
+
throw new SpawnfileError("compile_error", `Unable to resolve Docker context ${options.dockerContext} architecture: ${reason}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
22
56
|
export const runDockerBuild = async (invocation) => new Promise((resolve, reject) => {
|
|
23
57
|
const child = spawn(invocation.command, invocation.args, {
|
|
24
58
|
cwd: invocation.cwd,
|
|
@@ -38,12 +72,17 @@ export const runDockerBuild = async (invocation) => new Promise((resolve, reject
|
|
|
38
72
|
});
|
|
39
73
|
});
|
|
40
74
|
export const buildProject = async (inputPath, options = {}) => {
|
|
75
|
+
const targetArchitecture = options.containerArchitecture ?? await resolveDockerBuildArchitecture(options);
|
|
41
76
|
const compileResult = await compileProject(inputPath, {
|
|
42
77
|
clean: options.clean,
|
|
78
|
+
containerArchitecture: targetArchitecture,
|
|
43
79
|
outputDirectory: options.outputDirectory
|
|
44
80
|
});
|
|
45
81
|
const imageTag = options.imageTag ?? createDefaultImageTag(resolveImageTagRoot(inputPath));
|
|
46
|
-
const invocation = createDockerBuildInvocation(compileResult.outputDirectory, imageTag,
|
|
82
|
+
const invocation = createDockerBuildInvocation(compileResult.outputDirectory, imageTag, {
|
|
83
|
+
dockerCommand: options.dockerCommand,
|
|
84
|
+
dockerContext: options.dockerContext
|
|
85
|
+
});
|
|
47
86
|
await (options.buildRunner ?? runDockerBuild)(invocation);
|
|
48
87
|
return {
|
|
49
88
|
...compileResult,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CompileReport } from "../report/index.js";
|
|
2
|
+
import { type MoltnetTargetArchitecture } from "./moltnetBinaries.js";
|
|
2
3
|
export interface CompileProjectOptions {
|
|
3
4
|
clean?: boolean;
|
|
5
|
+
containerArchitecture?: MoltnetTargetArchitecture;
|
|
4
6
|
outputDirectory?: string;
|
|
5
7
|
}
|
|
6
8
|
export interface CompileProjectResult {
|
|
@@ -232,7 +232,9 @@ export const compileProject = async (inputPath, options = {}) => {
|
|
|
232
232
|
}
|
|
233
233
|
const moltnetArtifacts = await generateMoltnetArtifacts(plan);
|
|
234
234
|
const hasStagedMoltnetBinaries = moltnetArtifacts
|
|
235
|
-
? await stageMoltnetBinaries(outputDirectory
|
|
235
|
+
? await stageMoltnetBinaries(outputDirectory, {
|
|
236
|
+
architecture: options.containerArchitecture
|
|
237
|
+
})
|
|
236
238
|
: false;
|
|
237
239
|
await injectMoltnetWorkspaceFiles(outputDirectory, compiledNodes, moltnetArtifacts);
|
|
238
240
|
const generatedAt = new Date().toISOString();
|