@flydocs/cli 0.6.0-alpha.24 → 0.6.0-alpha.26
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/dist/cli.js +1342 -205
- package/package.json +1 -1
- package/template/.claude/hooks/session-start.py +117 -1
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +13 -4
- package/template/.claude/skills/flydocs-workflow/scripts/graph_build.py +2 -2
- package/template/.flydocs/config.json +1 -1
- package/template/.flydocs/version +1 -1
- package/template/manifest.json +1 -1
package/package.json
CHANGED
|
@@ -84,7 +84,17 @@ def get_active_issue_context() -> str | None:
|
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
def get_config_freshness() -> str | None:
|
|
87
|
-
"""Warn if validation cache is stale or missing."""
|
|
87
|
+
"""Warn if validation cache is stale or missing (v1 configs)."""
|
|
88
|
+
# v2 configs use get_config_freshness_v2() instead
|
|
89
|
+
config_file = Path('.flydocs/config.json')
|
|
90
|
+
if config_file.exists():
|
|
91
|
+
try:
|
|
92
|
+
config = json.loads(config_file.read_text())
|
|
93
|
+
if config.get('configFormat') == 2:
|
|
94
|
+
return None # Skip v1 check for v2 configs
|
|
95
|
+
except (json.JSONDecodeError, OSError):
|
|
96
|
+
pass
|
|
97
|
+
|
|
88
98
|
cache_file = Path('.flydocs/validation-cache.json')
|
|
89
99
|
if not cache_file.exists():
|
|
90
100
|
return None
|
|
@@ -102,6 +112,104 @@ def get_config_freshness() -> str | None:
|
|
|
102
112
|
return None
|
|
103
113
|
|
|
104
114
|
|
|
115
|
+
def get_config_freshness_v2() -> str | None:
|
|
116
|
+
"""Check config freshness via config/check endpoint (v2 configs only, FLY-540)."""
|
|
117
|
+
config_file = Path('.flydocs/config.json')
|
|
118
|
+
if not config_file.exists():
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
config = json.loads(config_file.read_text())
|
|
123
|
+
except (json.JSONDecodeError, OSError):
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
if config.get('configFormat') != 2 or config.get('tier') != 'cloud':
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# Resolve API key: env var > global credentials
|
|
130
|
+
api_key = os.environ.get('FLYDOCS_API_KEY')
|
|
131
|
+
if not api_key:
|
|
132
|
+
cred_file = Path.home() / '.flydocs' / 'credentials'
|
|
133
|
+
if cred_file.exists():
|
|
134
|
+
try:
|
|
135
|
+
cred = json.loads(cred_file.read_text())
|
|
136
|
+
api_key = cred.get('apiKey')
|
|
137
|
+
except (json.JSONDecodeError, OSError):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
if not api_key:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
# Call config/check — lightweight, ~50ms
|
|
144
|
+
base_url = os.environ.get('FLYDOCS_RELAY_URL', 'https://app.flydocs.ai/api/relay').rstrip('/')
|
|
145
|
+
try:
|
|
146
|
+
import urllib.request
|
|
147
|
+
req = urllib.request.Request(
|
|
148
|
+
f'{base_url}/config/check',
|
|
149
|
+
headers={
|
|
150
|
+
'Authorization': f'Bearer {api_key}',
|
|
151
|
+
'Accept': 'application/json',
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
155
|
+
data = json.loads(resp.read().decode('utf-8'))
|
|
156
|
+
except Exception:
|
|
157
|
+
# Server unreachable — silently continue, never block a session
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
if data.get('needsSync'):
|
|
161
|
+
stale_fields: list[str] = []
|
|
162
|
+
local_config_version = config.get('configVersion', 0)
|
|
163
|
+
if data.get('configVersion', 0) > local_config_version:
|
|
164
|
+
stale_fields.append('config')
|
|
165
|
+
if data.get('templateVersion', 0) > 0:
|
|
166
|
+
stale_fields.append('templates')
|
|
167
|
+
if data.get('contextVersion', 0) > 0:
|
|
168
|
+
stale_fields.append('context')
|
|
169
|
+
detail = f' ({", ".join(stale_fields)})' if stale_fields else ''
|
|
170
|
+
return f'Config updated{detail} — run `flydocs sync`'
|
|
171
|
+
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def check_skills_manifest() -> str | None:
|
|
176
|
+
"""Alert-mode skills manifest validation (FLY-541)."""
|
|
177
|
+
config_file = Path('.flydocs/config.json')
|
|
178
|
+
if not config_file.exists():
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
config = json.loads(config_file.read_text())
|
|
183
|
+
except (json.JSONDecodeError, OSError):
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
expected = config.get('skills', {}).get('installed', [])
|
|
187
|
+
if not expected:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
skills_dir = Path('.claude/skills')
|
|
191
|
+
if not skills_dir.is_dir():
|
|
192
|
+
return f'Skills directory missing — run `flydocs sync`'
|
|
193
|
+
|
|
194
|
+
# Check for installed directories
|
|
195
|
+
try:
|
|
196
|
+
installed = {d.name for d in skills_dir.iterdir() if d.is_dir()}
|
|
197
|
+
except OSError:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
expected_set = set(expected)
|
|
201
|
+
missing = expected_set - installed
|
|
202
|
+
unexpected = installed - expected_set
|
|
203
|
+
|
|
204
|
+
warnings: list[str] = []
|
|
205
|
+
if missing:
|
|
206
|
+
warnings.append(f'Missing skills: {", ".join(sorted(missing))} — run `flydocs sync`')
|
|
207
|
+
if unexpected:
|
|
208
|
+
warnings.append(f'Unexpected skills: {", ".join(sorted(unexpected))}')
|
|
209
|
+
|
|
210
|
+
return ' | '.join(warnings) if warnings else None
|
|
211
|
+
|
|
212
|
+
|
|
105
213
|
def main() -> None:
|
|
106
214
|
"""Main hook execution."""
|
|
107
215
|
try:
|
|
@@ -129,6 +237,14 @@ def main() -> None:
|
|
|
129
237
|
if freshness:
|
|
130
238
|
parts.append(freshness)
|
|
131
239
|
|
|
240
|
+
freshness_v2 = get_config_freshness_v2()
|
|
241
|
+
if freshness_v2:
|
|
242
|
+
parts.append(freshness_v2)
|
|
243
|
+
|
|
244
|
+
skills_warning = check_skills_manifest()
|
|
245
|
+
if skills_warning:
|
|
246
|
+
parts.append(skills_warning)
|
|
247
|
+
|
|
132
248
|
if not parts:
|
|
133
249
|
print('{}')
|
|
134
250
|
sys.exit(0)
|
|
@@ -14,13 +14,14 @@ The service descriptor serves two roles from a single file:
|
|
|
14
14
|
where things are: entry points, shared types, build system, package boundaries.
|
|
15
15
|
Not exported cross-repo.
|
|
16
16
|
|
|
17
|
-
Generated by the user's coding agent during `/flydocs-setup` Phase 1.5
|
|
17
|
+
Generated by the user's coding agent during `/flydocs-setup` Phase 1.5, or by the
|
|
18
|
+
server-side AI scanning pipeline (v2).
|
|
18
19
|
|
|
19
20
|
## Schema
|
|
20
21
|
|
|
21
22
|
```typescript
|
|
22
23
|
interface ServiceDescriptor {
|
|
23
|
-
version: 1;
|
|
24
|
+
version: 1 | 2;
|
|
24
25
|
name: string; // Human-readable service name
|
|
25
26
|
repoSlug: string; // owner/repo format (matches workspace.repoSlug)
|
|
26
27
|
purpose: string; // One-sentence description of what this service does
|
|
@@ -32,6 +33,10 @@ interface ServiceDescriptor {
|
|
|
32
33
|
|
|
33
34
|
// Intra-repo orientation (what THIS repo's agent uses)
|
|
34
35
|
structure: ServiceStructure;
|
|
36
|
+
|
|
37
|
+
// v2 fields (present when version is 2, optional for backward compat)
|
|
38
|
+
generatedBy?: "server" | "agent"; // Who generated this descriptor
|
|
39
|
+
generatedAt?: string; // ISO 8601 timestamp of generation
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
interface ApiSurface {
|
|
@@ -63,12 +68,16 @@ interface PackageInfo {
|
|
|
63
68
|
|
|
64
69
|
## Field Notes
|
|
65
70
|
|
|
66
|
-
- `version` is
|
|
71
|
+
- `version` is `1` (agent-generated, legacy) or `2` (supports server generation).
|
|
72
|
+
All consumers accept both versions. The v2 fields are additive — v1 descriptors
|
|
73
|
+
remain valid and fully functional.
|
|
67
74
|
- `repoSlug` must match the slug registered in the workspace dashboard.
|
|
68
75
|
- `structure` is local-only — not pushed to relay or included in workspace composite.
|
|
69
76
|
- `apis` and `dependencies` create PROVIDES/CONSUMES edges in the graph.
|
|
70
77
|
- `stack` is a flat array of lowercase identifiers (framework names, languages).
|
|
71
|
-
-
|
|
78
|
+
- `generatedBy` distinguishes server-generated (AI scanning pipeline) from
|
|
79
|
+
agent-generated (local `/flydocs-setup`) descriptors. Absent on v1 descriptors.
|
|
80
|
+
- `generatedAt` tracks freshness for server-generated descriptors. ISO 8601 format.
|
|
72
81
|
|
|
73
82
|
## Examples
|
|
74
83
|
|
|
@@ -219,7 +219,7 @@ def scan_service_descriptor(root):
|
|
|
219
219
|
except (json.JSONDecodeError, OSError):
|
|
220
220
|
return nodes, edges
|
|
221
221
|
|
|
222
|
-
if data.get("version")
|
|
222
|
+
if data.get("version") not in (1, 2):
|
|
223
223
|
return nodes, edges
|
|
224
224
|
|
|
225
225
|
repo_slug = data.get("repoSlug", "")
|
|
@@ -290,7 +290,7 @@ def scan_sibling_descriptors(root):
|
|
|
290
290
|
except (json.JSONDecodeError, OSError):
|
|
291
291
|
continue
|
|
292
292
|
|
|
293
|
-
if data.get("version")
|
|
293
|
+
if data.get("version") not in (1, 2):
|
|
294
294
|
continue
|
|
295
295
|
|
|
296
296
|
repo_slug = data.get("repoSlug", "")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.0-alpha.
|
|
1
|
+
0.6.0-alpha.26
|
package/template/manifest.json
CHANGED