@flydocs/cli 0.6.0-alpha.23 → 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/.claude/skills/flydocs-workflow/scripts/workspace.py +44 -2
- package/template/.flydocs/config.json +1 -1
- package/template/.flydocs/version +1 -1
- package/template/CHANGELOG.md +16 -0
- 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", "")
|
|
@@ -50,8 +50,8 @@ CHECK_MESSAGES: dict[str, str] = {
|
|
|
50
50
|
"statusMapping": "Status mapping not configured — configure in FlyDocs dashboard",
|
|
51
51
|
"labelConfig": "Label config not configured — configure in FlyDocs dashboard",
|
|
52
52
|
"userIdentity": (
|
|
53
|
-
"Provider identity not linked —
|
|
54
|
-
"
|
|
53
|
+
"Provider identity not linked — link your identity in the FlyDocs dashboard "
|
|
54
|
+
"profile page, or run: workspace.py set-identity <provider> <your-account-id>"
|
|
55
55
|
),
|
|
56
56
|
"repos": "No repos linked — GitHub features won't work until you push and link a repo",
|
|
57
57
|
}
|
|
@@ -115,6 +115,39 @@ def _check_integrity(client: "FlyDocsClient") -> dict:
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def _try_auto_resolve_identity(client: "FlyDocsClient") -> bool:
|
|
119
|
+
"""Attempt to auto-resolve provider identity via get-me. Returns True if resolved."""
|
|
120
|
+
try:
|
|
121
|
+
result = client.relay.get("/auth/me")
|
|
122
|
+
except Exception:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
provider_id = result.get("providerId")
|
|
126
|
+
if not provider_id:
|
|
127
|
+
# Check providerIdentities array as fallback
|
|
128
|
+
identities = result.get("providerIdentities", [])
|
|
129
|
+
if identities:
|
|
130
|
+
provider_id = identities[0].get("providerId")
|
|
131
|
+
|
|
132
|
+
if not provider_id:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# Write me.json
|
|
136
|
+
me_data = {
|
|
137
|
+
"displayName": result.get("displayName"),
|
|
138
|
+
"email": result.get("email"),
|
|
139
|
+
"providerId": provider_id,
|
|
140
|
+
"provider": result.get("provider"),
|
|
141
|
+
"providerIdentities": result.get("providerIdentities", []),
|
|
142
|
+
"preferences": result.get("preferences", {}),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
me_path = client.project_root / ".flydocs" / "me.json"
|
|
146
|
+
me_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
me_path.write_text(json.dumps(me_data, indent=2) + "\n")
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
|
|
118
151
|
def cmd_validate(args: argparse.Namespace) -> None:
|
|
119
152
|
"""Validate workspace setup via GET /auth/config."""
|
|
120
153
|
client = get_client()
|
|
@@ -126,6 +159,15 @@ def cmd_validate(args: argparse.Namespace) -> None:
|
|
|
126
159
|
missing_keys: list[str] = config_response.get("missing", [])
|
|
127
160
|
warning_keys: list[str] = config_response.get("warnings", [])
|
|
128
161
|
|
|
162
|
+
# Auto-resolve identity if missing — try get-me before requiring manual step
|
|
163
|
+
if "userIdentity" in missing_keys or "userIdentity" in warning_keys:
|
|
164
|
+
if _try_auto_resolve_identity(client):
|
|
165
|
+
missing_keys = [k for k in missing_keys if k != "userIdentity"]
|
|
166
|
+
warning_keys = [k for k in warning_keys if k != "userIdentity"]
|
|
167
|
+
# Re-check validity — if userIdentity was the only missing item, we're valid now
|
|
168
|
+
if not missing_keys:
|
|
169
|
+
is_valid = True
|
|
170
|
+
|
|
129
171
|
# Build structured missing/warning lists with messages
|
|
130
172
|
missing = [
|
|
131
173
|
{"check": k, "action": CHECK_MESSAGES.get(k, DEFAULT_MESSAGE)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.0-alpha.
|
|
1
|
+
0.6.0-alpha.26
|
package/template/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ Versioning: [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.6.0-alpha.24] — 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Auto-resolve provider identity** — `workspace.py validate` now attempts
|
|
15
|
+
`GET /auth/me` before requiring manual `set-identity` step. If the relay
|
|
16
|
+
returns a provider identity, `me.json` is written automatically (FLY-520)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Friendlier identity fallback** — when auto-resolve fails, the validation
|
|
21
|
+
message now points to the dashboard profile page instead of showing a raw
|
|
22
|
+
script command
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
10
26
|
## [0.6.0-alpha.23] — 2026-03-29
|
|
11
27
|
|
|
12
28
|
### Changed
|
package/template/manifest.json
CHANGED