@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flydocs/cli",
3
- "version": "0.6.0-alpha.23",
3
+ "version": "0.6.0-alpha.26",
4
4
  "type": "module",
5
5
  "description": "FlyDocs AI CLI — install, setup, and manage FlyDocs projects",
6
6
  "bin": {
@@ -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 always `1`. Will increment on breaking schema changes.
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
- - Agent scans the codebase to populate all fields during setup Phase 1.5.
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") != 1:
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") != 1:
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 — run: "
54
- "python3 .claude/skills/flydocs-workflow/scripts/workspace.py set-identity <provider> <id>"
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,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.23",
2
+ "version": "0.6.0-alpha.26",
3
3
  "sourceRepo": "github.com/plastrlab/flydocs-core",
4
4
  "tier": "local",
5
5
  "setupComplete": false,
@@ -1 +1 @@
1
- 0.6.0-alpha.23
1
+ 0.6.0-alpha.26
@@ -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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.23",
2
+ "version": "0.6.0-alpha.26",
3
3
  "description": "FlyDocs Core - Manifest of all managed files",
4
4
  "repository": "github.com/plastrlab/flydocs-core",
5
5