@avantmedia/specforge 0.0.0 → 0.19.0-rc.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/README.md +57 -1
- package/bin/specforge-serve.mjs +58 -0
- package/lib/resolve.mjs +114 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
# SpecForge — headless web server
|
|
2
|
+
|
|
3
|
+
Browse [OpenSpec](https://github.com/Fission-AI/OpenSpec) workspaces in your
|
|
4
|
+
browser. This package ships `specforge-serve`, the standalone SpecForge web
|
|
5
|
+
server: one binary with the UI embedded, no separate assets to deploy.
|
|
6
|
+
|
|
7
|
+
Version 0.19.0-rc.1.
|
|
8
|
+
|
|
9
|
+
## Run it
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# In any directory containing an openspec/ workspace
|
|
13
|
+
npx @avantmedia/specforge@0.19.0-rc.1
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
It binds `127.0.0.1:4317` and serves the UI there.
|
|
17
|
+
|
|
18
|
+
For a server you intend to leave running — a remote dev box or a homelab
|
|
19
|
+
machine — install it rather than using `npx`, which re-resolves the package on
|
|
20
|
+
every invocation:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g @avantmedia/specforge@0.19.0-rc.1
|
|
24
|
+
specforge-serve
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Options
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
--bind <addr> interface to bind [env: SPECFORGE_WEB_BIND] (default: 127.0.0.1)
|
|
31
|
+
--port <port> port to listen on [env: SPECFORGE_WEB_PORT] (default: 4317)
|
|
32
|
+
--help print the full reference and exit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**`--bind` on a non-loopback interface publishes the workspace-reading API to
|
|
36
|
+
everyone who can reach the port, without authentication.** The default is
|
|
37
|
+
loopback; only widen it on a network you trust.
|
|
38
|
+
|
|
39
|
+
## Platforms
|
|
40
|
+
|
|
41
|
+
macOS (Apple Silicon and Intel), Linux (x64 and arm64), and Windows (x64). The
|
|
42
|
+
matching binary is selected automatically at install time — only one is
|
|
43
|
+
downloaded.
|
|
44
|
+
|
|
45
|
+
The Linux binaries are statically linked against musl, so they run on musl
|
|
46
|
+
distributions such as Alpine as well as on glibc distributions, including older
|
|
47
|
+
long-term-support releases.
|
|
48
|
+
|
|
49
|
+
## Notes
|
|
50
|
+
|
|
51
|
+
Installing through npm needs no macOS quarantine step: files extracted by a
|
|
52
|
+
package manager are not flagged the way a browser download is. The binaries are
|
|
53
|
+
**not** code-signed — packages carry npm build provenance, which attests where
|
|
54
|
+
they were built, not who authored them.
|
|
55
|
+
|
|
56
|
+
The desktop application and the terminal UI are distributed separately, from
|
|
57
|
+
[GitHub Releases](https://github.com/avantmedialtd/specforge/releases/latest).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `specforge-serve` entry point of the `@avantmedia/specforge` wrapper.
|
|
3
|
+
//
|
|
4
|
+
// This file does as little as possible: find the binary that the matching
|
|
5
|
+
// platform package installed, hand it every argument untouched, and get out of
|
|
6
|
+
// the way. It deliberately does not parse, validate, or rewrite arguments —
|
|
7
|
+
// `specforge-serve --help` is the binary's own help, and a flag added to the
|
|
8
|
+
// server must never need a change here to reach it.
|
|
9
|
+
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
exitCodeFor,
|
|
18
|
+
locateBinary,
|
|
19
|
+
supportedKeysFrom,
|
|
20
|
+
unsupportedMessage,
|
|
21
|
+
} from "../lib/resolve.mjs";
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
const binary = locateBinary({
|
|
27
|
+
platform: process.platform,
|
|
28
|
+
arch: process.arch,
|
|
29
|
+
resolvePackageJson: (specifier) => require.resolve(specifier),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (binary === null) {
|
|
33
|
+
const manifest = JSON.parse(
|
|
34
|
+
readFileSync(path.join(here, "..", "package.json"), "utf8"),
|
|
35
|
+
);
|
|
36
|
+
console.error(
|
|
37
|
+
unsupportedMessage(
|
|
38
|
+
process.platform,
|
|
39
|
+
process.arch,
|
|
40
|
+
supportedKeysFrom(manifest.optionalDependencies),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// `stdio: "inherit"` hands the real terminal to the server, so its startup
|
|
47
|
+
// address and its non-loopback bind warning reach the user directly rather than
|
|
48
|
+
// through a buffer this process would have to relay.
|
|
49
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
50
|
+
|
|
51
|
+
if (result.error) {
|
|
52
|
+
console.error(
|
|
53
|
+
`specforge-serve: failed to execute ${binary}\n${result.error.message}`,
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.exit(exitCodeFor(result));
|
package/lib/resolve.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Platform resolution for the `@avantmedia/specforge` wrapper.
|
|
2
|
+
//
|
|
3
|
+
// This module ships inside the published wrapper package, so it must be plain
|
|
4
|
+
// JavaScript that runs under a bare `node` with no build step — the repo's
|
|
5
|
+
// TypeScript config deliberately scopes `include` to `src`, and nothing here is
|
|
6
|
+
// compiled before publication.
|
|
7
|
+
//
|
|
8
|
+
// The wrapper carries no binary of its own. Each supported target is published
|
|
9
|
+
// as a separate `@avantmedia/specforge-<os>-<cpu>` package, declared in the
|
|
10
|
+
// wrapper's `optionalDependencies` and filtered by npm's `os`/`cpu` fields, so
|
|
11
|
+
// installing the wrapper downloads exactly one of them. Everything below is the
|
|
12
|
+
// logic that finds that one package at run time.
|
|
13
|
+
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
/** npm's platform key for a target: `process.platform` + `-` + `process.arch`. */
|
|
17
|
+
export function platformKey(platform, arch) {
|
|
18
|
+
return `${platform}-${arch}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The published platform package name for a platform key. */
|
|
22
|
+
export function packageNameFor(key) {
|
|
23
|
+
return `@avantmedia/specforge-${key}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The executable's file name, which carries an extension only on Windows. */
|
|
27
|
+
export function binaryNameFor(platform) {
|
|
28
|
+
return platform === "win32" ? "specforge-serve.exe" : "specforge-serve";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The platform keys this wrapper was published with.
|
|
33
|
+
*
|
|
34
|
+
* Read from the wrapper's own `optionalDependencies` rather than a hard-coded
|
|
35
|
+
* list: the generator writes that field and the platform packages from one
|
|
36
|
+
* table, so deriving the keys back out keeps a single source of truth and means
|
|
37
|
+
* a wrapper can never advertise support it did not ship.
|
|
38
|
+
*/
|
|
39
|
+
export function supportedKeysFrom(optionalDependencies) {
|
|
40
|
+
const prefix = packageNameFor("");
|
|
41
|
+
return Object.keys(optionalDependencies ?? {})
|
|
42
|
+
.filter((name) => name.startsWith(prefix))
|
|
43
|
+
.map((name) => name.slice(prefix.length))
|
|
44
|
+
.sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Locate the platform binary, or return `null` when no platform package is
|
|
49
|
+
* present.
|
|
50
|
+
*
|
|
51
|
+
* `null` covers two distinct situations that are indistinguishable here and
|
|
52
|
+
* identical to the caller: the host is a target we never published, or the host
|
|
53
|
+
* is supported but the package manager resolved the `optionalDependencies` to
|
|
54
|
+
* nothing. The latter is a recurring npm defect rather than a theoretical case,
|
|
55
|
+
* which is why a missing package is a value to handle and not an exception to
|
|
56
|
+
* let escape.
|
|
57
|
+
*
|
|
58
|
+
* `resolvePackageJson` is injected so the failure branches stay testable
|
|
59
|
+
* without publishing or installing anything.
|
|
60
|
+
*/
|
|
61
|
+
export function locateBinary({ platform, arch, resolvePackageJson }) {
|
|
62
|
+
const pkg = packageNameFor(platformKey(platform, arch));
|
|
63
|
+
let manifestPath;
|
|
64
|
+
try {
|
|
65
|
+
manifestPath = resolvePackageJson(`${pkg}/package.json`);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return path.join(path.dirname(manifestPath), "bin", binaryNameFor(platform));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The message shown when no binary could be located.
|
|
74
|
+
*
|
|
75
|
+
* It names the detected platform explicitly, because the most common report is
|
|
76
|
+
* "it doesn't work on my machine" from a container whose architecture the user
|
|
77
|
+
* has not thought about, and points at the release downloads so an unsupported
|
|
78
|
+
* platform still has a path forward.
|
|
79
|
+
*/
|
|
80
|
+
export function unsupportedMessage(platform, arch, supportedKeys) {
|
|
81
|
+
const key = platformKey(platform, arch);
|
|
82
|
+
const known = supportedKeys.includes(key);
|
|
83
|
+
|
|
84
|
+
const reason = known
|
|
85
|
+
? `The platform package ${packageNameFor(key)} is not installed.\n` +
|
|
86
|
+
`Your platform (${key}) is supported, so this is usually a package\n` +
|
|
87
|
+
`manager that resolved optional dependencies to nothing — a known npm\n` +
|
|
88
|
+
`issue. Reinstalling, or removing the lockfile and node_modules and\n` +
|
|
89
|
+
`installing again, normally fixes it.`
|
|
90
|
+
: `No SpecForge server build is published for your platform (${key}).\n` +
|
|
91
|
+
`Published platforms: ${supportedKeys.join(", ")}.`;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
`specforge-serve: could not find a binary to run.\n\n${reason}\n\n` +
|
|
95
|
+
`Prebuilt archives for every released platform are available at\n` +
|
|
96
|
+
`https://github.com/avantmedialtd/specforge/releases/latest`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Translate a finished child process into this process's exit code.
|
|
102
|
+
*
|
|
103
|
+
* A child killed by a signal reports `status: null`, and exiting 0 there would
|
|
104
|
+
* report success for a server that was terminated. The shell convention of
|
|
105
|
+
* `128 + signal` keeps `Ctrl-C` distinguishable from a clean shutdown.
|
|
106
|
+
*/
|
|
107
|
+
export function exitCodeFor({ status, signal }) {
|
|
108
|
+
if (typeof status === "number") return status;
|
|
109
|
+
if (signal) {
|
|
110
|
+
const numbers = { SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15 };
|
|
111
|
+
return 128 + (numbers[signal] ?? 0);
|
|
112
|
+
}
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avantmedia/specforge",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Placeholder reserving this name for npm trusted publishing. Do not install.",
|
|
3
|
+
"version": "0.19.0-rc.1",
|
|
5
4
|
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/avantmedialtd/specforge.git"
|
|
8
|
+
},
|
|
7
9
|
"homepage": "https://github.com/avantmedialtd/specforge#readme",
|
|
8
|
-
"
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/avantmedialtd/specforge/issues"
|
|
12
|
+
},
|
|
13
|
+
"description": "Run the SpecForge headless web server — browse OpenSpec workspaces in a browser.",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"specforge",
|
|
16
|
+
"openspec",
|
|
17
|
+
"spec-driven",
|
|
18
|
+
"cli",
|
|
19
|
+
"server"
|
|
20
|
+
],
|
|
21
|
+
"bin": {
|
|
22
|
+
"specforge-serve": "bin/specforge-serve.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"lib",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"optionalDependencies": {
|
|
33
|
+
"@avantmedia/specforge-darwin-arm64": "0.19.0-rc.1",
|
|
34
|
+
"@avantmedia/specforge-darwin-x64": "0.19.0-rc.1",
|
|
35
|
+
"@avantmedia/specforge-linux-x64": "0.19.0-rc.1",
|
|
36
|
+
"@avantmedia/specforge-linux-arm64": "0.19.0-rc.1",
|
|
37
|
+
"@avantmedia/specforge-win32-x64": "0.19.0-rc.1"
|
|
38
|
+
}
|
|
9
39
|
}
|