feat(cli): prepare and validate workspace documents offline
Reuse the runtime catalog and workspace parsers in a standalone helper paired with tht. Add document templates, safe diagnostics, local Evidence checks, native bundle builds, shared CLI fixtures and IT/EN preparation guides. Record the approved document-first specification and ticket breakdown. Refs #43.
This commit is contained in:
@@ -1,5 +1,46 @@
|
||||
# Native installation CLI
|
||||
|
||||
## Offline workspace documents (issue #43)
|
||||
|
||||
`tht workspace prepare --directory NEW_PATH --id ID --name NAME [--language en|it]`
|
||||
creates an ad hoc workspace repository template. `tht workspace validate --directory
|
||||
PATH [--json]` checks the working documents without an installation descriptor or
|
||||
services. Both commands invoke the packaged sibling `tht-workspace-documents`, which
|
||||
compiles the canonical backend YAML/Zod parsers with its runtime. Neither Node nor
|
||||
Bun is required on the user's computer. Validation never writes documents or Git
|
||||
configuration. See the [IT](../../docs/install/standalone-manual-it.md) and
|
||||
[EN](../../docs/install/standalone-manual-en.md) guides for the correction loop and
|
||||
explicit runtime checks that local validation cannot satisfy.
|
||||
|
||||
Maintainer build (Go from `go.mod`, Node/npm for build only):
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
npm ci
|
||||
npm run build:workspace-tools
|
||||
# Cross-compile the complete platform pairs:
|
||||
npm run build:workspace-tools -- --all
|
||||
```
|
||||
|
||||
The output is `dist/workspace-tools/<os>-<arch>/` containing `tht[.exe]`,
|
||||
`tht-workspace-documents[.exe]`, `SHA256SUMS` and `build.json`. Individual targets:
|
||||
`windows-amd64`, `linux-amd64`, `linux-arm64`, `darwin-amd64`, `darwin-arm64`.
|
||||
Bun is pinned in `backend/package-lock.json`; compilation embeds the runtime, and
|
||||
cross-compilation may download the selected Bun target. Deliver the complete pair
|
||||
from one build, verify checksums and keep the executables together. The previous
|
||||
`build-tht.sh` / `install-tht.sh` single-binary path remains for existing operator
|
||||
commands; it does not package this helper. Release publication is tracked separately
|
||||
in issue #46; building a Windows/Linux artifact does not establish acceptance there.
|
||||
|
||||
Run the same public CLI fixture corpus against the native pair, with subprocess
|
||||
`PATH` deliberately empty:
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
THT_WORKSPACE_TEST_CLI=/absolute/path/dist/workspace-tools/darwin-arm64/tht \
|
||||
npx vitest run test/workspace-documents-cli.test.ts
|
||||
```
|
||||
|
||||
## Shell configuration
|
||||
|
||||
The schema-v2 `thothii-installation.yaml` accepts this optional section:
|
||||
|
||||
@@ -85,6 +85,10 @@ Commands:
|
||||
pi maintenance recover --yes
|
||||
Verify a terminal installation, remove stale lifecycle files, and clear maintenance.
|
||||
pi logs Show the latest 200 sanitized core log lines (bounded; no follow mode).
|
||||
workspace prepare --directory NEW_PATH --id ID --name NAME [--language en|it] [--json]
|
||||
Prepare workspace documents locally, before installation.
|
||||
workspace validate --directory PATH [--json]
|
||||
Validate local workspace documents without starting services.
|
||||
workspace inspect --workspace ID [--json]
|
||||
workspace pull [--json]
|
||||
Pull and activate the configured workspace repository.
|
||||
@@ -141,6 +145,9 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
return commandUsageError(stderr, fmt.Sprintf("unknown command %q", command))
|
||||
}
|
||||
if command == "workspace" && len(commandArgs) > 0 && (commandArgs[0] == "prepare" || commandArgs[0] == "validate") {
|
||||
return workspaceDocumentsCommand(ctx, commandArgs, stdout, stderr)
|
||||
}
|
||||
workingDirectory, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "tht: current directory is unavailable: %s\n", output.Sanitize(err.Error(), nil))
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Resolve only the packaged sibling, never an executable from the workspace or PATH.
|
||||
func workspaceDocumentsCommand(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
executable, err := os.Executable()
|
||||
if err == nil {
|
||||
executable, err = filepath.EvalSymlinks(executable)
|
||||
}
|
||||
if err == nil {
|
||||
name := "tht-workspace-documents"
|
||||
if runtime.GOOS == "windows" {
|
||||
name += ".exe"
|
||||
}
|
||||
command := exec.CommandContext(ctx, filepath.Join(filepath.Dir(executable), name), args...)
|
||||
command.Stdout, command.Stderr = stdout, stderr
|
||||
err = command.Run()
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) && exitError.ExitCode() >= 0 {
|
||||
return exitError.ExitCode()
|
||||
}
|
||||
}
|
||||
const correction = "Install tht and tht-workspace-documents from the same platform bundle in the same directory, then retry."
|
||||
if slices.Contains(args, "--json") {
|
||||
_ = json.NewEncoder(stdout).Encode(map[string]any{
|
||||
"schema_version": 1, "scope": "local-documents", "ok": false,
|
||||
"workspaces": []any{}, "deferred_checks": []string{},
|
||||
"issues": []map[string]string{{"document": "CLI", "field": "$", "code": "workspace_helper_unavailable", "correction": correction}},
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(stderr, correction)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWorkspaceDocumentsNeedsPackagedHelperNotInstallation(t *testing.T) {
|
||||
t.Setenv("PATH", "")
|
||||
t.Setenv("THOTHII_INSTALLATION", "/nonexistent/installation.yaml")
|
||||
for _, action := range []string{"prepare", "validate"} {
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"workspace", action, "--directory", t.TempDir(), "--json"}, &stdout, &stderr)
|
||||
var report struct {
|
||||
OK bool `json:"ok"`
|
||||
Issues []struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"issues"`
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &report); err != nil {
|
||||
t.Fatalf("missing structured helper error: status=%d stdout=%s stderr=%s", status, &stdout, &stderr)
|
||||
}
|
||||
if status != 1 || report.OK || len(report.Issues) != 1 || report.Issues[0].Code != "workspace_helper_unavailable" || stderr.Len() != 0 {
|
||||
t.Fatalf("unexpected missing-helper result: status=%d stdout=%s stderr=%s", status, &stdout, &stderr)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user