siesa-agents 2.1.79 → 2.1.81
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.
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sa-init-observability
|
|
3
|
+
description: 'Initialize a `.siesa-project` file that consolidates observability metrics across multi-repo logical projects (docs/backend/frontend sub-repos cloned side by side). Use whenever the user wants to set up observability for a new project, mentions `.siesa-project`, asks how to consolidate fragmented `project_id` values in GCP, sees metrics split across `business-*-docs-*`/`business-*-backend-*`/`business-*-frontend-*` repos, or starts working in a project that follows the multi-repo convention. Run this once per logical project, before workflows emit telemetry, so `sa-emit.js` reports a unified `project_id` instead of one per sub-repo.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Init Observability — `.siesa-project` Setup
|
|
7
|
+
|
|
8
|
+
**Goal:** Create a `.siesa-project` file in the correct parent directory so `sa-emit.js` reports a single, stable `project_id` for every workflow that runs inside any sub-repo of the same logical project.
|
|
9
|
+
|
|
10
|
+
**When this matters:** Projects following the Siesa multi-repo convention split docs, backend and frontend into separate GitHub repos (`business-pos-docs-pdv`, `business-pos-backend-pdv`, `business-pos-frontend-pdv`). Without `.siesa-project`, `sa-emit.js` falls back to `git remote get-url origin` and emits three different `project_id` values for what is conceptually one project. That fragments dashboards in GCP and inflates per-engineer epic counts because the same epic gets counted once per sub-repo it touches.
|
|
11
|
+
|
|
12
|
+
## When to use this skill
|
|
13
|
+
|
|
14
|
+
Trigger immediately when the user:
|
|
15
|
+
|
|
16
|
+
- Says "initialize observability", "set up observability", "init `.siesa-project`", or "configure metrics" for a project
|
|
17
|
+
- Mentions that GCP shows the same engineer/epic across multiple `business-*-docs-*` / `business-*-backend-*` / `business-*-frontend-*` rows
|
|
18
|
+
- Asks how to make several sub-repos report under one logical project name
|
|
19
|
+
- Has just cloned a multi-repo project (e.g. `docs/` + `apps/backend/` + `apps/frontend/`) and is about to run BMAD workflows that emit telemetry
|
|
20
|
+
- References the `.siesa-project` file by name
|
|
21
|
+
|
|
22
|
+
Do **not** trigger for: editing existing skills, changing OTLP endpoints, debugging a specific event payload (that is `sa-emit.js` territory), or anything unrelated to the `.siesa-project` mechanism.
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- The user is sitting inside a git repository that belongs to a logical Siesa project (typically a clone of `business-*-{docs|backend|frontend}-*`).
|
|
27
|
+
- Node.js is on the PATH (the scripts are invoked via `node`).
|
|
28
|
+
- Two helper scripts live in the observability scripts dir and are mirrored to the npm copy:
|
|
29
|
+
- `sa-init-env.js` — ensures `~/.claude/observability/.env` has the OTLP variables sa-emit needs.
|
|
30
|
+
- `sa-init-project.js` — detects the multi-repo convention and writes `.siesa-project`.
|
|
31
|
+
- Source-of-truth path is `_siesa-agents/observability/scripts/`; the npm mirror is `siesa-agents/observability/scripts/`. Prefer the source-of-truth path when both exist.
|
|
32
|
+
|
|
33
|
+
If a script is missing, stop and tell the user — do not improvise the logic inline. The scripts are the authoritative source for slug derivation and env scaffolding.
|
|
34
|
+
|
|
35
|
+
## Workflow
|
|
36
|
+
|
|
37
|
+
The skill is a three-step orchestration: **ensure global env → detect → write**. Step 0 is idempotent and almost always silent. Steps 1 and 2 do the real work for the `.siesa-project` file.
|
|
38
|
+
|
|
39
|
+
### Step 0 — Ensure global env
|
|
40
|
+
|
|
41
|
+
Run `sa-init-env.js` first, every time the skill is invoked:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
node <observability-scripts>/sa-init-env.js
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The script verifies that `~/.claude/observability/.env` exists and declares both `SA_OTLP_ENDPOINT` and `SA_OTLP_HEADERS`. It does **not** modify `sa-emit.js` and does **not** copy values from any project `.env` — it only seeds clearly-fake placeholders that the user must replace before workflows emit real telemetry.
|
|
48
|
+
|
|
49
|
+
Parse the JSON `status` field:
|
|
50
|
+
|
|
51
|
+
- **`status: "already-configured"`** — Both variables are already present. Do not mention this to the user unless they explicitly asked about env setup; it is the silent happy path. Continue to Step 1.
|
|
52
|
+
- **`status: "created"`** — The file did not exist; the script wrote a fresh template at `env_path`. Tell the user once: "Created `~/.claude/observability/.env` with placeholder values. Edit it with your real OTLP endpoint and headers before running workflows that emit telemetry." Then continue to Step 1.
|
|
53
|
+
- **`status: "updated"`** — The file existed but was missing one of the two variables; placeholders were appended for `appended_vars`. Tell the user which vars were added and remind them to replace the placeholders. Continue to Step 1.
|
|
54
|
+
- **`status: "error"`** — Surface the error verbatim and stop. Do not proceed to Step 1 — if the env scaffolding is broken, the project-level setup is the wrong place to spend the user's attention.
|
|
55
|
+
|
|
56
|
+
The placeholders the script writes are intentionally bogus (`https://your-otel-collector.example.com`, `Authorization=Bearer YOUR_TOKEN_HERE`). They will not connect to anything. The user is expected to receive real values from the observability team and paste them in.
|
|
57
|
+
|
|
58
|
+
### Step 1 — Detect
|
|
59
|
+
|
|
60
|
+
Run `sa-init-project.js` from the user's current directory (or from any directory inside the target sub-repo if the user told you where they want to initialize):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
node <observability-scripts>/sa-init-project.js --detect
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Parse the JSON object printed to stdout. The `status` field selects the next branch.
|
|
67
|
+
|
|
68
|
+
**`status: "ready"`** — Multi-repo convention detected. The JSON contains everything needed to act:
|
|
69
|
+
|
|
70
|
+
- `proposed_slug` — the logical project name to write (e.g. `pos-pdv`)
|
|
71
|
+
- `recommended_dir` — the parent directory chosen by the script (1 level up for `docs` clones, 2 levels up for `backend`/`frontend` clones)
|
|
72
|
+
- `convention_breakdown` — how the slug was derived (`module`, `role`, `rest`)
|
|
73
|
+
|
|
74
|
+
Proceed straight to Step 2 with `proposed_slug` and `recommended_dir`. Do **not** ask the user to confirm — the user has already accepted the auto-detection policy by invoking this skill.
|
|
75
|
+
|
|
76
|
+
Single-repo projects also return `status: "ready"` (with `convention_match: false`). The proposed slug is the repo name lowercased and sanitized to kebab-case (e.g. `Siesa-Agents` → `siesa-agents`) and `recommended_dir` is the git root itself. Proceed to Step 2 the same way — no separate branch.
|
|
77
|
+
|
|
78
|
+
**`status: "already-configured"`** — A `.siesa-project` exists above the current git root. Stop and report `existing_siesa_project.path` and `existing_siesa_project.value` to the user. Do not overwrite implicitly. If the user replies that they want it changed, run `--write` with `--force`, the new slug, and the same directory.
|
|
79
|
+
|
|
80
|
+
**`status: "no-git-repo"`** — The cwd is not inside a git repo. Tell the user to `cd` into the clone they want to initialize and re-run. Do not invent a directory.
|
|
81
|
+
|
|
82
|
+
**`status: "no-remote"`** — Git repo without an `origin` remote. Stop and ask the user for a slug + directory before proceeding, since the script has no signal to auto-derive a name.
|
|
83
|
+
|
|
84
|
+
### Step 2 — Write
|
|
85
|
+
|
|
86
|
+
When `status == "ready"`, run write immediately with the values from the detect output — no user prompt in between:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
node <observability-scripts>/sa-init-project.js --write --slug "<proposed_slug>" --dir "<recommended_dir>"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Parse the JSON response:
|
|
93
|
+
|
|
94
|
+
- **`status: "written"`** — Success. Report a single concise line to the user: the absolute `path` and the slug that was written. Optionally suggest the verification command from Step 3.
|
|
95
|
+
- **`status: "exists"`** — A file is already there with a different value (the detect step missed it because the user pointed at a non-default dir, or a race). Show `current_value` vs `requested_slug` and ask the user whether to overwrite. If yes, re-run with `--force`.
|
|
96
|
+
- **`status: "error"`** — Surface the error message verbatim and stop.
|
|
97
|
+
|
|
98
|
+
For the `already-configured` and `no-remote` branches where the user explicitly opts into writing, gather slug + directory inline and run the same `--write` command.
|
|
99
|
+
|
|
100
|
+
The file body written by `--write` is always `project_id=<slug>\n` (key=value form). `sa-emit.js` accepts both this format and a legacy bare-slug line for backward compatibility, so existing `.siesa-project` files keep working.
|
|
101
|
+
|
|
102
|
+
### Step 3 — Verify (optional)
|
|
103
|
+
|
|
104
|
+
Suggest the user run a quick sanity check from any sub-repo of the project:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
node <observability-scripts>/sa-emit.js --event workflow.started --story "0-verify" --phase create-story 2>&1 | grep project_id
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If the buffered event shows `project_id=<confirmed-slug>`, the consolidation works. If it still shows the git-remote-derived value, the user is running from a directory that is **not** a descendant of the chosen parent — point them at the right cwd.
|
|
111
|
+
|
|
112
|
+
## Edge cases
|
|
113
|
+
|
|
114
|
+
- **Nested logical projects.** If the user runs the skill inside a directory that is already under an existing `.siesa-project`, the script reports `already-configured`. Do not stack `.siesa-project` files — the lowest one wins during walk-up, which can silently mask the consolidation.
|
|
115
|
+
- **Slugs with dots, underscores or uppercase.** The script rejects them. Stick to lowercase + dashes (`pos-pdv`, `finance-billing`). If the user pushes back, explain that GCP labels work best with kebab-case and that other Siesa scripts assume this shape.
|
|
116
|
+
- **Convention without `business-` prefix.** The parser handles both `business-pos-backend-pdv` and a hypothetical `pos-backend-pdv` — both produce `pos-pdv`. Future modules (`finance-*`, `hcm-*`) work without any code change.
|
|
117
|
+
- **Parent dir that is itself a git repo.** Sometimes the docs repo *is* the logical parent (e.g. `comercial/` is a git repo and `comercial/apps/{backend,frontend}/` are clones inside it). That is fine — writing `.siesa-project` in `comercial/` will (a) commit-able in the docs repo, and (b) walked up correctly by `sa-emit.js` from `comercial/apps/backend/`. Mention this to the user so they can decide whether to commit the file (recommended, since it documents the convention for teammates).
|
|
118
|
+
- **Windows path separators.** The script uses `path.join`, so output paths may contain backslashes. Treat them as opaque and pass them back to `--write --dir` verbatim.
|
|
119
|
+
|
|
120
|
+
## Convention reference
|
|
121
|
+
|
|
122
|
+
The Siesa multi-repo naming convention is:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
business-<module>-<role>-<rest>
|
|
126
|
+
└ org prefix
|
|
127
|
+
└ logical module (pos, finance, hcm, …)
|
|
128
|
+
└ role: docs | backend | frontend
|
|
129
|
+
└ project-specific suffix (pdv, tomapedido, billing, …)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The logical slug collapses `<role>` away: `business-pos-backend-pdv` → `pos-pdv`.
|
|
133
|
+
|
|
134
|
+
For repo names that do not contain `docs`/`backend`/`frontend`, the script still returns `status: "ready"` but with `convention_match: false`. The slug is derived from the repo name (lowercased, sanitized to kebab-case) and the file is written at the git root of that single repo.
|
|
135
|
+
|
|
136
|
+
## Examples
|
|
137
|
+
|
|
138
|
+
**Example 1 — Happy path on a `pos-pdv` clone (env already configured)**
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
User: I just cloned business-pos-backend-pdv into comercial/apps/backend. Set up observability.
|
|
142
|
+
Model: <runs sa-init-env.js — status "already-configured", silent>
|
|
143
|
+
<runs sa-init-project.js --detect from comercial/apps/backend/>
|
|
144
|
+
<reads proposed_slug="pos-pdv", recommended_dir=C:\…\comercial>
|
|
145
|
+
<runs --write --slug pos-pdv --dir C:\…\comercial without asking>
|
|
146
|
+
Wrote C:\…\comercial\.siesa-project. sa-emit.js will now report project_id="pos-pdv".
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Example 1b — Same flow on a fresh machine (env missing)**
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
User: Set up observability for this clone.
|
|
153
|
+
Model: <runs sa-init-env.js — status "created">
|
|
154
|
+
Created ~/.claude/observability/.env with placeholder values. Edit it with your real OTLP endpoint and headers before running workflows that emit telemetry.
|
|
155
|
+
<continues with detect + write as in Example 1>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Example 2 — Single-repo project**
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
User: Set up .siesa-project for Siesa-Agents.
|
|
162
|
+
Model: <runs sa-init-env.js — silent>
|
|
163
|
+
<runs --detect from Siesa-Agents/>
|
|
164
|
+
<reads proposed_slug="siesa-agents", recommended_dir=C:\…\Siesa-Agents, convention_match=false>
|
|
165
|
+
<runs --write --slug siesa-agents --dir C:\…\Siesa-Agents>
|
|
166
|
+
Wrote C:\…\Siesa-Agents\.siesa-project (project_id=siesa-agents). sa-emit.js will now report project_id="siesa-agents" instead of "SiesaTeams/Siesa-Agents".
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Example 3 — Already configured**
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
User: Init observability here.
|
|
173
|
+
Model: <runs --detect>
|
|
174
|
+
A .siesa-project already exists at C:\…\comercial\.siesa-project with value "pos-pdv". sa-emit.js is already consolidating under that slug. No action needed unless you want to change the value.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Reminders
|
|
178
|
+
|
|
179
|
+
- Step 0 (`sa-init-env.js`) always runs first and is idempotent. Skip the user-facing message when its status is `already-configured`; surface it only on `created` / `updated` / `error`.
|
|
180
|
+
- On `status: "ready"` from `sa-init-project.js --detect`, write immediately using `proposed_slug` and `recommended_dir` — no confirmation prompt. The user invoked this skill knowing it auto-creates the file.
|
|
181
|
+
- Never invent a slug. If the detect step returns `single-repo`, `no-remote`, or any branch other than `ready`, stop and let the user drive the next decision.
|
|
182
|
+
- Never call `--write --force` automatically. Overwriting an existing `.siesa-project` always requires an explicit user request.
|
|
183
|
+
- Do not modify `sa-emit.js` or other observability scripts from this skill — its only side effects are (a) creating `~/.claude/observability/.env` with placeholders if absent and (b) creating or overwriting (with consent) a single `.siesa-project` file.
|
|
184
|
+
- The scripts' JSON output is the contract. If a future field appears (e.g. an extra status), surface it to the user rather than guessing what it means.
|
package/package.json
CHANGED
|
@@ -134,17 +134,38 @@ const stateDir = path.join(os.homedir(), '.claude', 'observability')
|
|
|
134
134
|
const epicIdMatch = story.match(/^(\d+)-/)
|
|
135
135
|
const epicId = epicIdMatch ? epicIdMatch[1] : 'unknown'
|
|
136
136
|
|
|
137
|
+
function parseSiesaProject(content) {
|
|
138
|
+
// Preferred format: a single `project_id=<slug>` line. For backward compatibility a bare
|
|
139
|
+
// slug on its own line is also accepted. `#` introduces a comment. Returns the resolved
|
|
140
|
+
// slug, or null if nothing usable was found.
|
|
141
|
+
if (!content) return null
|
|
142
|
+
let firstBare = null
|
|
143
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
144
|
+
const line = raw.trim()
|
|
145
|
+
if (!line || line.startsWith('#')) continue
|
|
146
|
+
const eq = line.indexOf('=')
|
|
147
|
+
if (eq !== -1) {
|
|
148
|
+
const key = line.slice(0, eq).trim()
|
|
149
|
+
const val = line.slice(eq + 1).trim()
|
|
150
|
+
if (key === 'project_id' && val) return val
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (firstBare === null) firstBare = line
|
|
154
|
+
}
|
|
155
|
+
return firstBare
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
function findSiesaProject(startDir) {
|
|
138
|
-
// Walk up from startDir looking for a `.siesa-project` file. Returns
|
|
139
|
-
//
|
|
159
|
+
// Walk up from startDir looking for a `.siesa-project` file. Returns the project_id slug
|
|
160
|
+
// it declares, or null. Lets a parent dir override the git-remote-derived project_id —
|
|
140
161
|
// useful when several sub-repos (docs/backend/frontend) belong to the same logical project.
|
|
141
162
|
let cur = path.resolve(startDir)
|
|
142
163
|
for (let i = 0; i < 40; i++) {
|
|
143
164
|
const candidate = path.join(cur, '.siesa-project')
|
|
144
165
|
if (fs.existsSync(candidate)) {
|
|
145
166
|
try {
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
167
|
+
const slug = parseSiesaProject(fs.readFileSync(candidate, 'utf8'))
|
|
168
|
+
if (slug) return slug
|
|
148
169
|
} catch (_) {}
|
|
149
170
|
}
|
|
150
171
|
const parent = path.dirname(cur)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sa-init-env.js — Ensure `~/.claude/observability/.env` exists with placeholders for the two
|
|
3
|
+
// OTLP variables the Siesa observability stack needs:
|
|
4
|
+
// - SA_OTLP_ENDPOINT
|
|
5
|
+
// - SA_OTLP_HEADERS
|
|
6
|
+
//
|
|
7
|
+
// This script is idempotent. If the file already contains both variables, it exits without
|
|
8
|
+
// touching anything. If the file is missing, it creates it with clearly-fake placeholder
|
|
9
|
+
// values that the user is expected to edit before workflows emit real telemetry. If the file
|
|
10
|
+
// exists but is missing one of the two variables, the missing one is appended — existing
|
|
11
|
+
// values for unrelated keys are preserved exactly.
|
|
12
|
+
//
|
|
13
|
+
// Note: this script does NOT change how sa-emit.js loads configuration. sa-emit.js continues
|
|
14
|
+
// to load only the project-local .env (`process.cwd()/.env`). This script keeps
|
|
15
|
+
// `~/.claude/observability/.env` as a discoverable per-machine template so engineers know
|
|
16
|
+
// which variables they need without having to read sa-emit.js.
|
|
17
|
+
//
|
|
18
|
+
// Modes:
|
|
19
|
+
// <no args> Default = --ensure. Creates the file (and parent dir) when needed.
|
|
20
|
+
// --check Read-only check. Reports the current state without writing anything.
|
|
21
|
+
//
|
|
22
|
+
// Exit codes:
|
|
23
|
+
// 0 Success (ensure created/updated/no-op, or check ran cleanly)
|
|
24
|
+
// 1 Write or filesystem error
|
|
25
|
+
|
|
26
|
+
'use strict'
|
|
27
|
+
|
|
28
|
+
const fs = require('fs')
|
|
29
|
+
const os = require('os')
|
|
30
|
+
const path = require('path')
|
|
31
|
+
|
|
32
|
+
const TARGET_VARS = ['SA_OTLP_ENDPOINT', 'SA_OTLP_HEADERS']
|
|
33
|
+
|
|
34
|
+
const PLACEHOLDERS = {
|
|
35
|
+
SA_OTLP_ENDPOINT: 'https://your-otel-collector.example.com',
|
|
36
|
+
SA_OTLP_HEADERS: 'Authorization=Bearer YOUR_TOKEN_HERE',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const out = {}
|
|
41
|
+
for (const a of argv) {
|
|
42
|
+
if (a.startsWith('--')) out[a.slice(2)] = true
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emit(obj) {
|
|
48
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + '\n')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveTarget() {
|
|
52
|
+
// sa-emit.js uses os.homedir() + '.claude' + 'observability' for its state dir; we mirror
|
|
53
|
+
// that path so engineers see a single, predictable location across both tools.
|
|
54
|
+
const dir = path.join(os.homedir(), '.claude', 'observability')
|
|
55
|
+
const file = path.join(dir, '.env')
|
|
56
|
+
return { dir, file }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse an existing .env file into an ordered list of entries plus a key index. We track
|
|
60
|
+
// order so we can rewrite the file deterministically without scrambling the user's edits.
|
|
61
|
+
function readEnvFile(filePath) {
|
|
62
|
+
if (!fs.existsSync(filePath)) {
|
|
63
|
+
return { exists: false, lines: [], keys: new Set() }
|
|
64
|
+
}
|
|
65
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
66
|
+
const lines = raw.split(/\r?\n/)
|
|
67
|
+
const keys = new Set()
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const trimmed = line.trim()
|
|
70
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
71
|
+
const eq = trimmed.indexOf('=')
|
|
72
|
+
if (eq === -1) continue
|
|
73
|
+
keys.add(trimmed.slice(0, eq).trim())
|
|
74
|
+
}
|
|
75
|
+
return { exists: true, lines, keys, raw }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findMissingVars(keys) {
|
|
79
|
+
return TARGET_VARS.filter(v => !keys.has(v))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildFreshFile() {
|
|
83
|
+
return [
|
|
84
|
+
'# Siesa observability — OTLP exporter configuration.',
|
|
85
|
+
'# This file is a per-machine template. Replace the placeholder values with the real',
|
|
86
|
+
'# endpoint and headers you received from the observability team. sa-emit.js itself',
|
|
87
|
+
'# reads only the project-local .env; this file documents which variables are required',
|
|
88
|
+
'# and gives you one canonical place to keep your personal copy of them.',
|
|
89
|
+
'',
|
|
90
|
+
`SA_OTLP_ENDPOINT=${PLACEHOLDERS.SA_OTLP_ENDPOINT}`,
|
|
91
|
+
`SA_OTLP_HEADERS=${PLACEHOLDERS.SA_OTLP_HEADERS}`,
|
|
92
|
+
'',
|
|
93
|
+
].join('\n')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function appendMissing(existingRaw, missing) {
|
|
97
|
+
// Append missing vars at the bottom under a clear delimiter so the user can spot the new
|
|
98
|
+
// additions and replace the placeholders.
|
|
99
|
+
const needsLeadingNewline = existingRaw.length > 0 && !existingRaw.endsWith('\n')
|
|
100
|
+
const parts = [existingRaw]
|
|
101
|
+
if (needsLeadingNewline) parts.push('\n')
|
|
102
|
+
parts.push('\n# --- added by sa-init-env.js (placeholders, edit before use) ---\n')
|
|
103
|
+
for (const v of missing) {
|
|
104
|
+
parts.push(`${v}=${PLACEHOLDERS[v]}\n`)
|
|
105
|
+
}
|
|
106
|
+
return parts.join('')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function runCheck() {
|
|
110
|
+
const { dir, file } = resolveTarget()
|
|
111
|
+
const env = readEnvFile(file)
|
|
112
|
+
if (!env.exists) {
|
|
113
|
+
emit({
|
|
114
|
+
status: 'missing',
|
|
115
|
+
message: `~/.claude/observability/.env does not exist. Run without --check to create it with placeholders.`,
|
|
116
|
+
env_path: file,
|
|
117
|
+
env_dir: dir,
|
|
118
|
+
target_vars: TARGET_VARS,
|
|
119
|
+
present_vars: [],
|
|
120
|
+
missing_vars: TARGET_VARS.slice(),
|
|
121
|
+
})
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
const missing = findMissingVars(env.keys)
|
|
125
|
+
if (missing.length === 0) {
|
|
126
|
+
emit({
|
|
127
|
+
status: 'already-configured',
|
|
128
|
+
message: `~/.claude/observability/.env already declares both SA_OTLP_ENDPOINT and SA_OTLP_HEADERS.`,
|
|
129
|
+
env_path: file,
|
|
130
|
+
env_dir: dir,
|
|
131
|
+
target_vars: TARGET_VARS,
|
|
132
|
+
present_vars: TARGET_VARS.slice(),
|
|
133
|
+
missing_vars: [],
|
|
134
|
+
})
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
emit({
|
|
138
|
+
status: 'partial',
|
|
139
|
+
message: `~/.claude/observability/.env exists but is missing: ${missing.join(', ')}. Run without --check to append placeholders for the missing variables.`,
|
|
140
|
+
env_path: file,
|
|
141
|
+
env_dir: dir,
|
|
142
|
+
target_vars: TARGET_VARS,
|
|
143
|
+
present_vars: TARGET_VARS.filter(v => env.keys.has(v)),
|
|
144
|
+
missing_vars: missing,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function runEnsure() {
|
|
149
|
+
const { dir, file } = resolveTarget()
|
|
150
|
+
|
|
151
|
+
if (!fs.existsSync(dir)) {
|
|
152
|
+
try {
|
|
153
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
154
|
+
} catch (err) {
|
|
155
|
+
emit({ status: 'error', message: `Failed to create ${dir}: ${err.message}`, env_path: file })
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const env = readEnvFile(file)
|
|
161
|
+
|
|
162
|
+
if (!env.exists) {
|
|
163
|
+
try {
|
|
164
|
+
fs.writeFileSync(file, buildFreshFile(), { encoding: 'utf8' })
|
|
165
|
+
} catch (err) {
|
|
166
|
+
emit({ status: 'error', message: `Failed to write ${file}: ${err.message}`, env_path: file })
|
|
167
|
+
process.exit(1)
|
|
168
|
+
}
|
|
169
|
+
emit({
|
|
170
|
+
status: 'created',
|
|
171
|
+
message: `Wrote ${file} with placeholder values. Edit the file and replace the placeholders with the real OTLP endpoint and headers before running workflows that emit telemetry.`,
|
|
172
|
+
env_path: file,
|
|
173
|
+
env_dir: dir,
|
|
174
|
+
target_vars: TARGET_VARS,
|
|
175
|
+
wrote_vars: TARGET_VARS.slice(),
|
|
176
|
+
placeholders: PLACEHOLDERS,
|
|
177
|
+
})
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const missing = findMissingVars(env.keys)
|
|
182
|
+
if (missing.length === 0) {
|
|
183
|
+
emit({
|
|
184
|
+
status: 'already-configured',
|
|
185
|
+
message: `~/.claude/observability/.env already declares both SA_OTLP_ENDPOINT and SA_OTLP_HEADERS. Nothing to do.`,
|
|
186
|
+
env_path: file,
|
|
187
|
+
env_dir: dir,
|
|
188
|
+
target_vars: TARGET_VARS,
|
|
189
|
+
present_vars: TARGET_VARS.slice(),
|
|
190
|
+
})
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// File exists but is missing one or more target vars. Append the missing ones with
|
|
195
|
+
// placeholders; never modify existing entries — the user may have already filled in
|
|
196
|
+
// real values for the keys that are present.
|
|
197
|
+
try {
|
|
198
|
+
fs.writeFileSync(file, appendMissing(env.raw, missing), { encoding: 'utf8' })
|
|
199
|
+
} catch (err) {
|
|
200
|
+
emit({ status: 'error', message: `Failed to update ${file}: ${err.message}`, env_path: file })
|
|
201
|
+
process.exit(1)
|
|
202
|
+
}
|
|
203
|
+
emit({
|
|
204
|
+
status: 'updated',
|
|
205
|
+
message: `Appended placeholders for missing variables to ${file}: ${missing.join(', ')}. Replace the placeholders with real values before running workflows that emit telemetry.`,
|
|
206
|
+
env_path: file,
|
|
207
|
+
env_dir: dir,
|
|
208
|
+
target_vars: TARGET_VARS,
|
|
209
|
+
appended_vars: missing,
|
|
210
|
+
placeholders: PLACEHOLDERS,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const args = parseArgs(process.argv.slice(2))
|
|
215
|
+
if (args['check']) {
|
|
216
|
+
runCheck()
|
|
217
|
+
} else {
|
|
218
|
+
runEnsure()
|
|
219
|
+
}
|
|
@@ -63,6 +63,27 @@ function tryExec(cmd, opts) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Parse a `.siesa-project` file body. Preferred format is `project_id=<slug>` (one key=value
|
|
67
|
+
// line, optional `#` comments). For backward compatibility a bare slug on its own line is
|
|
68
|
+
// also accepted. Returns the resolved slug, or null if nothing usable was found.
|
|
69
|
+
function parseSiesaProject(content) {
|
|
70
|
+
if (!content) return null
|
|
71
|
+
let firstBare = null
|
|
72
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
73
|
+
const line = raw.trim()
|
|
74
|
+
if (!line || line.startsWith('#')) continue
|
|
75
|
+
const eq = line.indexOf('=')
|
|
76
|
+
if (eq !== -1) {
|
|
77
|
+
const key = line.slice(0, eq).trim()
|
|
78
|
+
const val = line.slice(eq + 1).trim()
|
|
79
|
+
if (key === 'project_id' && val) return val
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if (firstBare === null) firstBare = line
|
|
83
|
+
}
|
|
84
|
+
return firstBare
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
// Walk up from `startDir` looking for an existing `.siesa-project`. Returns
|
|
67
88
|
// { path, value } of the first match found, or null.
|
|
68
89
|
function findExistingSiesaProject(startDir) {
|
|
@@ -71,8 +92,8 @@ function findExistingSiesaProject(startDir) {
|
|
|
71
92
|
const candidate = path.join(cur, '.siesa-project')
|
|
72
93
|
if (fs.existsSync(candidate)) {
|
|
73
94
|
try {
|
|
74
|
-
const
|
|
75
|
-
return { path: candidate, value
|
|
95
|
+
const value = parseSiesaProject(fs.readFileSync(candidate, 'utf8'))
|
|
96
|
+
return { path: candidate, value }
|
|
76
97
|
} catch (_) {
|
|
77
98
|
return { path: candidate, value: null }
|
|
78
99
|
}
|
|
@@ -186,15 +207,38 @@ function runDetect() {
|
|
|
186
207
|
const derived = deriveLogicalSlug(repoName)
|
|
187
208
|
|
|
188
209
|
if (!derived.matched) {
|
|
210
|
+
// Single-repo project: still write a `.siesa-project` so the project_id is explicit
|
|
211
|
+
// and self-documented. The slug is derived from the repo name, sanitized to kebab-case,
|
|
212
|
+
// and the file lands at the git root of the current project.
|
|
213
|
+
const singleRepoSlug = (repoName || '')
|
|
214
|
+
.toLowerCase()
|
|
215
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
216
|
+
.replace(/^-+|-+$/g, '')
|
|
217
|
+
if (!singleRepoSlug || !/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(singleRepoSlug)) {
|
|
218
|
+
emit({
|
|
219
|
+
status: 'no-remote',
|
|
220
|
+
message: 'Could not derive a valid kebab-case slug from the repo name. Pass --slug explicitly to write a .siesa-project.',
|
|
221
|
+
git_root: gitRoot,
|
|
222
|
+
remote_url: remoteUrl,
|
|
223
|
+
remote_repo: repoName,
|
|
224
|
+
})
|
|
225
|
+
process.exit(2)
|
|
226
|
+
}
|
|
189
227
|
emit({
|
|
190
|
-
status: '
|
|
191
|
-
message: '
|
|
228
|
+
status: 'ready',
|
|
229
|
+
message: 'Single-repo project detected. The .siesa-project file will be written at the git root with a slug derived from the repo name.',
|
|
192
230
|
git_root: gitRoot,
|
|
193
231
|
remote_url: remoteUrl,
|
|
194
232
|
remote_repo: repoName,
|
|
233
|
+
convention_match: false,
|
|
234
|
+
proposed_slug: singleRepoSlug,
|
|
235
|
+
convention_breakdown: null,
|
|
236
|
+
parent_candidates: [{ path: gitRoot, label: 'git root', levelsUp: 0 }],
|
|
237
|
+
recommended_dir: gitRoot,
|
|
195
238
|
fallback_project_id: remoteParsed ? `${remoteParsed.org}/${remoteParsed.repo}` : null,
|
|
239
|
+
next_step_example: `node sa-init-project.js --write --slug "${singleRepoSlug}" --dir "${gitRoot}"`,
|
|
196
240
|
})
|
|
197
|
-
process.exit(
|
|
241
|
+
process.exit(0)
|
|
198
242
|
}
|
|
199
243
|
|
|
200
244
|
const candidates = listParentCandidates(gitRoot)
|
|
@@ -256,7 +300,7 @@ function runWrite(args) {
|
|
|
256
300
|
const target = path.join(resolvedDir, '.siesa-project')
|
|
257
301
|
if (fs.existsSync(target) && !force) {
|
|
258
302
|
let current = null
|
|
259
|
-
try { current = fs.readFileSync(target, 'utf8')
|
|
303
|
+
try { current = parseSiesaProject(fs.readFileSync(target, 'utf8')) } catch (_) {}
|
|
260
304
|
emit({
|
|
261
305
|
status: 'exists',
|
|
262
306
|
message: '.siesa-project already exists at this location. Pass --force to overwrite.',
|
|
@@ -268,7 +312,7 @@ function runWrite(args) {
|
|
|
268
312
|
}
|
|
269
313
|
|
|
270
314
|
try {
|
|
271
|
-
fs.writeFileSync(target, normalizedSlug
|
|
315
|
+
fs.writeFileSync(target, `project_id=${normalizedSlug}\n`, { encoding: 'utf8' })
|
|
272
316
|
} catch (err) {
|
|
273
317
|
emit({ status: 'error', message: `Failed to write ${target}: ${err.message}` })
|
|
274
318
|
process.exit(3)
|