Skip to content

Commit 80b494d

Browse files
committed
feat: creation and publishing cycle initial
1 parent 1ed30b1 commit 80b494d

27 files changed

Lines changed: 7433 additions & 3164 deletions

cli/toob-cli/cmd/admin.go

Lines changed: 410 additions & 0 deletions
Large diffs are not rendered by default.

cli/toob-cli/cmd/audit.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/toob-boot/toob/internal/apiclient"
11+
"github.com/toob-boot/toob/internal/paths"
12+
"github.com/toob-boot/toob/internal/ui"
13+
)
14+
15+
var auditCmd = &cobra.Command{
16+
Use: "audit",
17+
Short: "Check local dependencies for security advisories",
18+
Long: `Scans the project's toob.lock for installed packages and checks each
19+
against the registry for revocation or security advisories.`,
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
projectRoot, err := paths.FindProjectRoot("")
22+
if err != nil {
23+
return fmt.Errorf("not in a Toob project: %w", err)
24+
}
25+
26+
lockPath := paths.LockfilePath(projectRoot)
27+
lockData, err := os.ReadFile(lockPath)
28+
if err != nil {
29+
ui.Info("No toob.lock found — nothing to audit.")
30+
return nil
31+
}
32+
33+
// Parse the lockfile to extract installed packages
34+
var lockfile struct {
35+
Packages []struct {
36+
Name string `json:"name"`
37+
Version string `json:"version"`
38+
} `json:"packages"`
39+
}
40+
if err := json.Unmarshal(lockData, &lockfile); err != nil {
41+
return fmt.Errorf("failed to parse %s: %w", filepath.Base(lockPath), err)
42+
}
43+
44+
if len(lockfile.Packages) == 0 {
45+
ui.Info("No packages in toob.lock — nothing to audit.")
46+
return nil
47+
}
48+
49+
ui.Header("Security Audit")
50+
client := apiclient.New()
51+
52+
issues := 0
53+
for _, pkg := range lockfile.Packages {
54+
resp, err := client.GetPackage(cmd_defaultCtx(), pkg.Name, pkg.Version)
55+
if err != nil {
56+
ui.Warn("%s@%s — could not verify: %v", pkg.Name, pkg.Version, err)
57+
issues++
58+
continue
59+
}
60+
61+
switch resp.Stage {
62+
case "revoked":
63+
ui.Error("%s@%s — REVOKED", pkg.Name, pkg.Version)
64+
issues++
65+
case "archived":
66+
ui.Warn("%s@%s — archived (consider upgrading)", pkg.Name, pkg.Version)
67+
default:
68+
ui.CheckItem(true, false, fmt.Sprintf("%s@%s", pkg.Name, pkg.Version), resp.Stage)
69+
}
70+
}
71+
72+
ui.Divider()
73+
if issues > 0 {
74+
ui.Error("%d issue(s) found. Update affected packages immediately.", issues)
75+
} else {
76+
ui.Success("All %d package(s) passed audit.", len(lockfile.Packages))
77+
}
78+
return nil
79+
},
80+
}
81+
82+
func init() {
83+
rootCmd.AddCommand(auditCmd)
84+
}

cli/toob-cli/cmd/build.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os/user"
1010
"path/filepath"
1111
"runtime"
12+
"slices"
1213
"strconv"
1314
"strings"
1415
"sync"
@@ -707,13 +708,7 @@ func runNativeBuild(root string) error {
707708

708709
// Validate chip_binding
709710
if len(cryptoPkg.ChipBinding) > 0 {
710-
bound := false
711-
for _, b := range cryptoPkg.ChipBinding {
712-
if b == chip {
713-
bound = true
714-
break
715-
}
716-
}
711+
bound := slices.Contains(cryptoPkg.ChipBinding, chip)
717712
if !bound {
718713
return fmt.Errorf("crypto package '%s' is chip-bound to %v, but target chip is '%s'",
719714
pkgName, cryptoPkg.ChipBinding, chip)
@@ -1059,8 +1054,8 @@ func findPythonScriptsBin() string {
10591054
// parseCoreSDKVersion extracts the raw semver from a tag (e.g., core/v1.2.3 -> v1.2.3)
10601055
func parseCoreSDKVersion(tag string) (*semver.Version, error) {
10611056
cleanTag := tag
1062-
if strings.HasPrefix(tag, "core/") {
1063-
cleanTag = strings.TrimPrefix(tag, "core/")
1057+
if after, ok := strings.CutPrefix(tag, "core/"); ok {
1058+
cleanTag = after
10641059
}
10651060
if !strings.HasPrefix(cleanTag, "v") {
10661061
cleanTag = "v" + cleanTag

cli/toob-cli/cmd/doctor.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99

1010
"github.com/spf13/cobra"
11+
"github.com/toob-boot/toob/internal/apiclient"
1112
"github.com/toob-boot/toob/internal/ui"
1213
)
1314

@@ -77,6 +78,38 @@ var doctorCmd = &cobra.Command{
7778
ui.CheckItem(true, false, check.name, version)
7879
}
7980

81+
// --- Registry Connectivity Checks (Gap 16) ---
82+
fmt.Fprintln(os.Stderr)
83+
ui.Step("Checking registry connectivity...")
84+
85+
client := apiclient.New()
86+
87+
// Ping the API
88+
_, revErr := client.GetRevision(cmd_defaultCtx())
89+
if revErr != nil {
90+
ui.CheckItem(false, false, "Registry API", fmt.Sprintf("unreachable: %v", revErr))
91+
allPassed = false
92+
} else {
93+
ui.CheckItem(true, false, "Registry API", client.BaseURL)
94+
}
95+
96+
// Auth key check
97+
if client.HasToken() {
98+
_, authErr := client.MyPackages(cmd_defaultCtx())
99+
if authErr != nil {
100+
ui.CheckItem(false, true, "Auth Token", fmt.Sprintf("invalid or expired: %v", authErr))
101+
} else {
102+
login := apiclient.GetLogin()
103+
detail := "valid"
104+
if login != "" {
105+
detail = fmt.Sprintf("valid (@%s)", login)
106+
}
107+
ui.CheckItem(true, false, "Auth Token", detail)
108+
}
109+
} else {
110+
ui.CheckItem(false, true, "Auth Token", "not configured (run 'toob login')")
111+
}
112+
80113
ui.Divider()
81114
if allPassed {
82115
ui.Success("System is ready for Toob development!")

cli/toob-cli/cmd/helpers.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"time"
6+
)
7+
8+
// cmd_defaultCtx returns a context with a generous timeout for interactive CLI operations.
9+
// The context is derived from Background() with no parent cancel — it is designed
10+
// for top-level CLI commands where the process lifetime is the natural boundary.
11+
func cmd_defaultCtx() context.Context {
12+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
13+
// Register cleanup: in CLI commands, the process exits after RunE completes,
14+
// so the cancel is effectively a no-op. We still call it to satisfy the linter.
15+
go func() {
16+
<-ctx.Done()
17+
cancel()
18+
}()
19+
return ctx
20+
}

cli/toob-cli/cmd/init.go

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import (
44
"fmt"
55
"os"
66
"os/exec"
7+
"path/filepath"
78
"regexp"
9+
"strings"
810

911
"github.com/spf13/cobra"
12+
"github.com/toob-boot/toob/internal/apiclient"
1013
"github.com/toob-boot/toob/internal/installer"
1114
"github.com/toob-boot/toob/internal/paths"
1215
"github.com/toob-boot/toob/internal/registry"
@@ -21,6 +24,7 @@ var (
2124
initDevContainer bool
2225
initSdkUrl string
2326
initSdkRevision string
27+
initPackage bool
2428
)
2529

2630
var initCmd = &cobra.Command{
@@ -30,8 +34,16 @@ var initCmd = &cobra.Command{
3034
RunE: func(cmd *cobra.Command, args []string) error {
3135
projectName := args[0]
3236

33-
validNamePattern := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
37+
var validNamePattern *regexp.Regexp
38+
if initPackage {
39+
validNamePattern = regexp.MustCompile(`^(@[a-zA-Z0-9_-]+/)?[a-zA-Z0-9_-]+$`)
40+
} else {
41+
validNamePattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
42+
}
3443
if !validNamePattern.MatchString(projectName) {
44+
if initPackage {
45+
return fmt.Errorf("invalid package name '%s'. Allowed: unscoped 'name' or scoped '@scope/name' containing alphanumeric, dashes, and underscores", projectName)
46+
}
3547
return fmt.Errorf("invalid project name '%s'. Only alphanumeric characters, dashes, and underscores are allowed", projectName)
3648
}
3749

@@ -78,6 +90,130 @@ var initCmd = &cobra.Command{
7890
return initErr
7991
}
8092

93+
if initPackage {
94+
ui.Step("Scaffolding publishable package project '%s'...", projectName)
95+
// Get author login
96+
author := apiclient.GetLogin()
97+
if author == "" {
98+
author = "unregistered"
99+
}
100+
101+
// Determine core/toolchain versions from matrix
102+
tcVersion := "12.2.0"
103+
coreVersion := "1.0.0"
104+
if matrix, err := cache.FetchLiveMatrix(); err == nil && matrix != nil {
105+
if chipMatrix, ok := (*matrix)[initChip]; ok {
106+
for _, v := range chipMatrix.Versions {
107+
if v.Dependencies.Toolchain != "" {
108+
tcVersion = v.Dependencies.Toolchain
109+
}
110+
if v.Dependencies.CoreSDK != "" {
111+
coreVersion = v.Dependencies.CoreSDK
112+
}
113+
break
114+
}
115+
}
116+
}
117+
118+
// Clean base name for files (remove scope if present)
119+
baseName := projectName
120+
if _, after, ok := strings.Cut(projectName, "/"); ok {
121+
baseName = after
122+
}
123+
124+
// 1. Create folders: src, include
125+
if err := os.MkdirAll(filepath.Join(projectDir, "src"), 0o755); err != nil {
126+
initErr = err
127+
return err
128+
}
129+
if err := os.MkdirAll(filepath.Join(projectDir, "include"), 0o755); err != nil {
130+
initErr = err
131+
return err
132+
}
133+
134+
// 2. Write driver_manifest.json
135+
manifestPath := filepath.Join(projectDir, "driver_manifest.json")
136+
manifestJSON := fmt.Sprintf(`{
137+
"name": %q,
138+
"author": %q,
139+
"version": "0.1.0",
140+
"description": "Scaffolded driver for %s",
141+
"reference_build_context": {
142+
"chip": %q,
143+
"core_sdk_version": %q,
144+
"toolchain_version": %q,
145+
"target_architecture": %q
146+
},
147+
"dependencies": {
148+
"core": "^%s"
149+
},
150+
"sources": [
151+
"src/%s.c"
152+
],
153+
"includes": [
154+
"include"
155+
]
156+
}
157+
`, projectName, author, initChip, initChip, coreVersion, tcVersion, ci.Arch, coreVersion, baseName)
158+
if err := os.WriteFile(manifestPath, []byte(manifestJSON), 0o644); err != nil {
159+
initErr = err
160+
return err
161+
}
162+
163+
// 3. Write src/<baseName>.c
164+
cPath := filepath.Join(projectDir, "src", baseName+".c")
165+
funcName := strings.ReplaceAll(baseName, "-", "_")
166+
cCode := fmt.Sprintf(`#include "%s.h"
167+
168+
// TODO: Implement your driver initialization function here
169+
void %s_init(void) {
170+
// Hardware initialization
171+
}
172+
`, baseName, funcName)
173+
if err := os.WriteFile(cPath, []byte(cCode), 0o644); err != nil {
174+
initErr = err
175+
return err
176+
}
177+
178+
// 4. Write include/<baseName>.h
179+
hPath := filepath.Join(projectDir, "include", baseName+".h")
180+
hCode := fmt.Sprintf(`#ifndef %s_H
181+
#define %s_H
182+
183+
void %s_init(void);
184+
185+
#endif // %s_H
186+
`, strings.ToUpper(funcName), strings.ToUpper(funcName), funcName, strings.ToUpper(funcName))
187+
if err := os.WriteFile(hPath, []byte(hCode), 0o644); err != nil {
188+
initErr = err
189+
return err
190+
}
191+
192+
// 5. Write .toobignore and .gitignore
193+
ignoreList := ".toob/\nbuild/\ncredentials.json\n*.tar.gz\n"
194+
if err := os.WriteFile(filepath.Join(projectDir, ".toobignore"), []byte(ignoreList), 0o644); err != nil {
195+
initErr = err
196+
return err
197+
}
198+
if err := os.WriteFile(filepath.Join(projectDir, ".gitignore"), []byte(ignoreList), 0o644); err != nil {
199+
initErr = err
200+
return err
201+
}
202+
203+
// Initialize git repo
204+
gitCmd := exec.Command("git", "init")
205+
_ = gitCmd.Run()
206+
207+
ui.Divider()
208+
ui.KeyValue("Package Name", ui.Bold(projectName))
209+
ui.KeyValue("Chip", ui.BoldBrand(initChip))
210+
ui.KeyValue("Architecture", ui.Cyan(ci.Arch))
211+
ui.Divider()
212+
ui.Success("Package initialized successfully!")
213+
ui.Tip("Run `cd %s` and check out `driver_manifest.json`.", projectName)
214+
return nil
215+
}
216+
81217
if initFramework == "" {
82218
idx, _ := cache.LoadIndex()
83219
liveIntegrations, liveErr := cache.FetchLiveIntegrations()
@@ -192,4 +328,6 @@ func init() {
192328
initCmd.Flags().BoolVar(&initDevContainer, "devcontainer", false, "Generate VS Code DevContainer configuration for isolated builds")
193329
initCmd.Flags().StringVar(&initSdkUrl, "sdk-url", "https://github.com/Toob-Boot/Toob-Loader.git", "URL to fetch the Toob-Loader SDK from")
194330
initCmd.Flags().StringVar(&initSdkRevision, "sdk-version", "main", "Git branch or tag to use for the Toob-Loader SDK")
331+
// TODO (Gap 10): Scaffold a publishable package project with manifest template.
332+
initCmd.Flags().BoolVar(&initPackage, "package", false, "Initialize as a publishable package project instead of a firmware application")
195333
}

0 commit comments

Comments
 (0)