@flydocs/cli 0.6.0-alpha.7 → 0.6.0-alpha.8

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 CHANGED
@@ -15,7 +15,7 @@ var CLI_VERSION, CLI_NAME, PACKAGE_NAME, POSTHOG_API_KEY;
15
15
  var init_constants = __esm({
16
16
  "src/lib/constants.ts"() {
17
17
  "use strict";
18
- CLI_VERSION = "0.6.0-alpha.7";
18
+ CLI_VERSION = "0.6.0-alpha.8";
19
19
  CLI_NAME = "flydocs";
20
20
  PACKAGE_NAME = "@flydocs/cli";
21
21
  POSTHOG_API_KEY = "phc_v1MSJTQDFkMS90CBh3mxIz3v8bYCCnKU6v1ir6bz0Xn";
@@ -293,10 +293,9 @@ function extractPreservedValues(config) {
293
293
  return {
294
294
  tier: config.tier,
295
295
  setupComplete: config.setupComplete ?? false,
296
- providerTeamId: config.provider?.teamId ?? null,
296
+ workspaceId: config.workspaceId ?? null,
297
297
  workspace: config.workspace ?? {},
298
298
  issueLabels: config.issueLabels ?? {},
299
- statusMapping: config.statusMapping ?? {},
300
299
  detectedStack: config.detectedStack ?? {},
301
300
  skills: config.skills ?? {},
302
301
  designSystem: config.designSystem ?? null,
@@ -312,21 +311,13 @@ async function mergeConfig(templateDir, version, tierFlag, preserved) {
312
311
  config.version = version;
313
312
  config.tier = tierFlag ?? preserved.tier;
314
313
  config.setupComplete = preserved.setupComplete;
315
- if (preserved.providerTeamId !== null) {
316
- if (!config.provider) {
317
- config.provider = { type: "linear", teamId: null };
318
- }
319
- config.provider.teamId = preserved.providerTeamId;
320
- }
314
+ config.workspaceId = preserved.workspaceId;
321
315
  if (Object.keys(preserved.workspace).length > 0) {
322
316
  config.workspace = preserved.workspace;
323
317
  }
324
318
  if (Object.keys(preserved.issueLabels).length > 0) {
325
319
  config.issueLabels = preserved.issueLabels;
326
320
  }
327
- if (Object.keys(preserved.statusMapping).length > 0) {
328
- config.statusMapping = preserved.statusMapping;
329
- }
330
321
  if (Object.keys(preserved.detectedStack).length > 0) {
331
322
  config.detectedStack = preserved.detectedStack;
332
323
  }
@@ -1888,6 +1879,21 @@ async function validateRelayKey(apiKey) {
1888
1879
  if (!data.valid) return { valid: false };
1889
1880
  return { valid: true, org: data.org ?? "your organization" };
1890
1881
  }
1882
+ async function fetchWorkspaces(apiKey) {
1883
+ const baseUrl = process.env.FLYDOCS_RELAY_URL?.replace(/\/$/, "") ?? "https://app.flydocs.ai/api/relay";
1884
+ const response = await fetch(`${baseUrl}/auth/workspaces`, {
1885
+ method: "GET",
1886
+ headers: {
1887
+ Authorization: `Bearer ${apiKey}`
1888
+ },
1889
+ signal: AbortSignal.timeout(15e3)
1890
+ });
1891
+ if (!response.ok) {
1892
+ throw new Error(`Failed to fetch workspaces: ${response.status}`);
1893
+ }
1894
+ const data = await response.json();
1895
+ return data;
1896
+ }
1891
1897
  async function validateLinearKey(apiKey) {
1892
1898
  const response = await fetch("https://api.linear.app/graphql", {
1893
1899
  method: "POST",
@@ -2074,6 +2080,8 @@ var init_install = __esm({
2074
2080
  }
2075
2081
  printInfo(`Tier: ${tier}`);
2076
2082
  await capture("install_tier_selected", { tier });
2083
+ let selectedWorkspaceId = null;
2084
+ let selectedWorkspaceName = null;
2077
2085
  if (tier === "cloud") {
2078
2086
  console.log();
2079
2087
  console.log(` ${pc6.bold("Connect to FlyDocs Cloud")}`);
@@ -2115,6 +2123,46 @@ var init_install = __esm({
2115
2123
  }
2116
2124
  const envFile = await storeEnvKey(targetDir, "FLYDOCS_API_KEY", apiKey);
2117
2125
  printStatus(`API key stored in ${pc6.dim(envFile)}`);
2126
+ console.log();
2127
+ printInfo("Fetching workspaces...");
2128
+ try {
2129
+ const workspaces = await fetchWorkspaces(apiKey);
2130
+ if (workspaces.length === 0) {
2131
+ printError(
2132
+ "No workspaces found. Create a workspace in the FlyDocs dashboard first, then re-run install."
2133
+ );
2134
+ process.exit(1);
2135
+ }
2136
+ if (workspaces.length === 1) {
2137
+ selectedWorkspaceId = workspaces[0].id;
2138
+ selectedWorkspaceName = workspaces[0].name;
2139
+ printStatus(
2140
+ `Workspace: ${pc6.bold(selectedWorkspaceName)} (auto-selected)`
2141
+ );
2142
+ } else {
2143
+ const workspaceChoice = await select({
2144
+ message: "Select a workspace",
2145
+ options: workspaces.map((ws) => ({
2146
+ value: ws.id,
2147
+ label: `${ws.name} (${ws.provider.type})`,
2148
+ hint: ws.team.name
2149
+ }))
2150
+ });
2151
+ if (isCancel3(workspaceChoice)) {
2152
+ cancel2("Installation cancelled.");
2153
+ process.exit(0);
2154
+ }
2155
+ selectedWorkspaceId = workspaceChoice;
2156
+ const chosen = workspaces.find((ws) => ws.id === selectedWorkspaceId);
2157
+ selectedWorkspaceName = chosen?.name ?? "Unknown";
2158
+ printStatus(`Workspace: ${pc6.bold(selectedWorkspaceName)}`);
2159
+ }
2160
+ } catch {
2161
+ printError(
2162
+ "Could not fetch workspaces. Check your network and try again."
2163
+ );
2164
+ process.exit(1);
2165
+ }
2118
2166
  }
2119
2167
  let skipConfigOverwrite = false;
2120
2168
  if (!await pathExists(join15(targetDir, ".git"))) {
@@ -2196,6 +2244,9 @@ var init_install = __esm({
2196
2244
  const configPath = join15(targetDir, ".flydocs", "config.json");
2197
2245
  if (!await pathExists(configPath)) {
2198
2246
  const config = await createFreshConfig(templateDir, version, tier);
2247
+ if (selectedWorkspaceId) {
2248
+ config.workspaceId = selectedWorkspaceId;
2249
+ }
2199
2250
  await writeConfig(targetDir, config);
2200
2251
  printStatus(`.flydocs/config.json (new, tier: ${tier})`);
2201
2252
  } else if (args.tier) {
@@ -2203,15 +2254,34 @@ var init_install = __esm({
2203
2254
  const existing = await readConfig(targetDir);
2204
2255
  existing.version = version;
2205
2256
  existing.tier = tier;
2257
+ if (selectedWorkspaceId) {
2258
+ existing.workspaceId = selectedWorkspaceId;
2259
+ }
2206
2260
  await writeConfig(targetDir, existing);
2207
2261
  printStatus(`.flydocs/config.json (tier updated: ${tier})`);
2208
2262
  } catch {
2209
2263
  const config = await createFreshConfig(templateDir, version, tier);
2264
+ if (selectedWorkspaceId) {
2265
+ config.workspaceId = selectedWorkspaceId;
2266
+ }
2210
2267
  await writeConfig(targetDir, config);
2211
2268
  printStatus(`.flydocs/config.json (recreated, tier: ${tier})`);
2212
2269
  }
2213
2270
  } else {
2214
- printWarning(".flydocs/config.json exists, preserving");
2271
+ if (selectedWorkspaceId) {
2272
+ try {
2273
+ const existing = await readConfig(targetDir);
2274
+ existing.workspaceId = selectedWorkspaceId;
2275
+ await writeConfig(targetDir, existing);
2276
+ printWarning(
2277
+ ".flydocs/config.json exists, preserving (workspace updated)"
2278
+ );
2279
+ } catch {
2280
+ printWarning(".flydocs/config.json exists, preserving");
2281
+ }
2282
+ } else {
2283
+ printWarning(".flydocs/config.json exists, preserving");
2284
+ }
2215
2285
  }
2216
2286
  console.log();
2217
2287
  console.log(`Installing skills (tier: ${tier})...`);
@@ -2810,10 +2880,9 @@ var init_update = __esm({
2810
2880
  let preserved = {
2811
2881
  tier: "cloud",
2812
2882
  setupComplete: false,
2813
- providerTeamId: null,
2883
+ workspaceId: null,
2814
2884
  workspace: {},
2815
2885
  issueLabels: {},
2816
- statusMapping: {},
2817
2886
  detectedStack: {},
2818
2887
  skills: {},
2819
2888
  designSystem: null,
@@ -2837,6 +2906,7 @@ var init_update = __esm({
2837
2906
  } else {
2838
2907
  effectiveTier = preserved.tier;
2839
2908
  }
2909
+ await ensureDirectories(targetDir, effectiveTier);
2840
2910
  console.log("Replacing framework directories...");
2841
2911
  await replaceDirectory(
2842
2912
  join16(templateDir, ".flydocs", "templates"),
@@ -3672,7 +3742,9 @@ var init_connect = __esm({
3672
3742
  }
3673
3743
  const wasLocal = config.tier === "local";
3674
3744
  config.tier = "cloud";
3675
- config.provider = config.provider ?? { type: null, teamId: null };
3745
+ const configRecord = config;
3746
+ delete configRecord.statusMapping;
3747
+ delete configRecord.provider;
3676
3748
  await writeConfig(targetDir, config);
3677
3749
  printStatus("Config updated to cloud tier");
3678
3750
  if (wasLocal) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flydocs/cli",
3
- "version": "0.6.0-alpha.7",
3
+ "version": "0.6.0-alpha.8",
4
4
  "type": "module",
5
5
  "description": "FlyDocs AI CLI — install, setup, and manage FlyDocs projects",
6
6
  "bin": {
@@ -74,6 +74,7 @@ All scripts: `python3 .claude/skills/flydocs-cloud/scripts/<script>`
74
74
  | `set_preferences.py` | `[--workspace ID] [--assignee self\|ID] [--display JSON]` | `{success, preferences}` — no flags = GET current; with flags = POST update |
75
75
  | `get_estimate_scale.py` | (no args) | `{scale, type}` — provider's valid estimate values (fixed or freeform) |
76
76
  | `refresh_labels.py` | `[--fix]` | `{valid, stale, details}` — validates config label IDs against relay; `--fix` updates stale IDs |
77
+ | `validate_setup.py` | (no args) | `{valid, checks, missing[]}` — validates workspace config, caches result, sets setupComplete |
77
78
 
78
79
  ### Script Notes
79
80
 
@@ -37,6 +37,12 @@ class FlyDocsClient:
37
37
  print("Set in environment or .env/.env.local file", file=sys.stderr)
38
38
  sys.exit(1)
39
39
 
40
+ self.workspace_id = self.config.get("workspaceId")
41
+ if not self.workspace_id:
42
+ print("ERROR: workspaceId not found in .flydocs/config.json", file=sys.stderr)
43
+ print("Run 'flydocs setup' to configure your workspace", file=sys.stderr)
44
+ sys.exit(1)
45
+
40
46
  self.base_url = self._resolve_base_url()
41
47
 
42
48
  def _load_config(self) -> dict:
@@ -93,6 +99,7 @@ class FlyDocsClient:
93
99
 
94
100
  headers = {
95
101
  "Authorization": f"Bearer {self.api_key}",
102
+ "X-Workspace": self.workspace_id,
96
103
  "Content-Type": "application/json",
97
104
  "Accept": "application/json",
98
105
  }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """Validate workspace setup via the FlyDocs Relay API.
3
+
4
+ Read-only validation that checks provider, team, status mapping, label config,
5
+ and user identity. Writes result to .flydocs/validation-cache.json and sets
6
+ setupComplete: true in config when all required checks pass.
7
+ """
8
+
9
+ import json
10
+ import sys
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).parent))
15
+ from flydocs_api import get_client, output_json, fail
16
+
17
+
18
+ # Required checks — failure blocks setup completion
19
+ REQUIRED_CHECKS = {
20
+ "provider": {
21
+ "test": lambda cfg: cfg.get("provider", {}).get("connected") is True,
22
+ "message": "No provider connected — configure in FlyDocs dashboard",
23
+ },
24
+ "team": {
25
+ "test": lambda cfg: bool(cfg.get("team", {}).get("id")),
26
+ "message": "No team selected — configure in FlyDocs dashboard",
27
+ },
28
+ "statusMapping": {
29
+ "test": lambda cfg: cfg.get("statusMapping", {}).get("configured") is True,
30
+ "message": "Status mapping not configured — configure in FlyDocs dashboard",
31
+ },
32
+ "labelConfig": {
33
+ "test": lambda cfg: cfg.get("labelConfig", {}).get("configured") is True,
34
+ "message": "Label config not configured — configure in FlyDocs dashboard",
35
+ },
36
+ }
37
+
38
+ # Optional checks — warn but don't block
39
+ OPTIONAL_CHECKS = {
40
+ "userIdentity": {
41
+ "test": lambda cfg: cfg.get("userIdentity", {}).get("linked") is True,
42
+ "message": (
43
+ "Provider identity not linked — run: "
44
+ "python3 .claude/skills/flydocs-cloud/scripts/set_identity.py <provider> <id>"
45
+ ),
46
+ },
47
+ }
48
+
49
+
50
+ def main() -> None:
51
+ client = get_client()
52
+
53
+ # Fetch workspace config from relay
54
+ config_response = client.get("/auth/config")
55
+
56
+ # Run checks
57
+ checks: dict[str, bool] = {}
58
+ valid: list[str] = []
59
+ missing: list[dict[str, str]] = []
60
+ warnings: list[dict[str, str]] = []
61
+
62
+ for name, check in REQUIRED_CHECKS.items():
63
+ passed = check["test"](config_response)
64
+ checks[name] = passed
65
+ if passed:
66
+ valid.append(name)
67
+ else:
68
+ missing.append({"check": name, "action": check["message"]})
69
+
70
+ for name, check in OPTIONAL_CHECKS.items():
71
+ passed = check["test"](config_response)
72
+ checks[name] = passed
73
+ if passed:
74
+ valid.append(name)
75
+ else:
76
+ warnings.append({"check": name, "action": check["message"]})
77
+
78
+ all_required_pass = len(missing) == 0
79
+
80
+ # Build workspace info from response
81
+ workspace = config_response.get("workspace", {})
82
+ provider_type = config_response.get("provider", {}).get("type", "unknown")
83
+
84
+ # Write validation cache
85
+ cache = {
86
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
87
+ "valid": all_required_pass,
88
+ "workspace": {
89
+ "id": workspace.get("id", ""),
90
+ "name": workspace.get("name", ""),
91
+ },
92
+ "provider": provider_type,
93
+ "checks": checks,
94
+ "missing": [m["check"] for m in missing],
95
+ }
96
+
97
+ cache_path = client.project_root / ".flydocs" / "validation-cache.json"
98
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
99
+ with open(cache_path, "w") as f:
100
+ json.dump(cache, f, indent=2)
101
+ f.write("\n")
102
+
103
+ # If all required checks pass, set setupComplete in config
104
+ if all_required_pass:
105
+ config_path = client.config_path
106
+ if config_path.exists():
107
+ with open(config_path, "r") as f:
108
+ local_config = json.load(f)
109
+ else:
110
+ local_config = {}
111
+
112
+ local_config["setupComplete"] = True
113
+ with open(config_path, "w") as f:
114
+ json.dump(local_config, f, indent=2)
115
+ f.write("\n")
116
+
117
+ # Output structured report
118
+ report: dict = {
119
+ "valid": all_required_pass,
120
+ "checks": checks,
121
+ "passed": valid,
122
+ }
123
+ if missing:
124
+ report["missing"] = missing
125
+ if warnings:
126
+ report["warnings"] = warnings
127
+ if all_required_pass:
128
+ report["setupComplete"] = True
129
+
130
+ output_json(report)
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
@@ -1,18 +1,16 @@
1
1
  {
2
- "version": "0.6.0-alpha.7",
2
+ "version": "0.6.0-alpha.8",
3
3
  "sourceRepo": "github.com/plastrlab/flydocs-core",
4
4
  "tier": "local",
5
5
  "setupComplete": false,
6
+ "workspaceId": null,
6
7
  "paths": {
7
8
  "content": "flydocs"
8
9
  },
9
- "provider": {
10
- "type": null,
11
- "teamId": null
12
- },
13
10
  "workspace": {
14
11
  "activeProjects": [],
15
12
  "defaultMilestoneId": null,
13
+ "repoSlug": null,
16
14
  "product": {
17
15
  "name": null,
18
16
  "labelIds": [],
@@ -21,7 +19,7 @@
21
19
  }
22
20
  },
23
21
  "issueLabels": {
24
- "_note": "Run /flydocs-setup to populate these from your provider",
22
+ "_note": "Run /flydocs-setup to populate these from your workspace",
25
23
  "category": {
26
24
  "feature": null,
27
25
  "bug": null,
@@ -50,18 +48,6 @@
50
48
  "installed": [],
51
49
  "custom": []
52
50
  },
53
- "statusMapping": {
54
- "BACKLOG": "Backlog",
55
- "READY": "Todo",
56
- "IMPLEMENTING": "In Progress",
57
- "BLOCKED": "Blocked",
58
- "REVIEW": "In Review",
59
- "TESTING": "QA",
60
- "COMPLETE": "Done",
61
- "ARCHIVED": "Archived",
62
- "CANCELED": "Canceled",
63
- "DUPLICATE": "Duplicate"
64
- },
65
51
  "designSystem": null,
66
52
  "aiLabor": {
67
53
  "enabled": false,
@@ -168,15 +168,32 @@ def get_flydocs_version() -> str | None:
168
168
 
169
169
 
170
170
  def get_setup_nudge() -> str | None:
171
- """Check if setup has been completed, return nudge if not."""
171
+ """Check if setup has been completed, return nudge if not.
172
+
173
+ Reads validation cache (written by validate_setup.py) for specific
174
+ missing items. Falls back to generic nudge if cache is absent.
175
+ """
172
176
  config_file = Path('.flydocs/config.json')
173
177
  if not config_file.exists():
174
178
  return None
175
179
  try:
176
180
  config = json.loads(config_file.read_text())
177
- # Only nudge if field is explicitly set to false (not missing)
178
- if 'setupComplete' in config and config['setupComplete'] is False:
179
- return '[Setup incomplete — run /flydocs-setup to configure your project]'
181
+ if config.get('setupComplete') is not False:
182
+ return None
183
+
184
+ # Check validation cache for specific missing items
185
+ cache_file = Path('.flydocs/validation-cache.json')
186
+ if cache_file.exists():
187
+ try:
188
+ cache = json.loads(cache_file.read_text())
189
+ missing = cache.get('missing', [])
190
+ if missing:
191
+ items = ', '.join(missing)
192
+ return f'[Setup incomplete — missing: {items}. Run /flydocs-setup or fix in dashboard]'
193
+ except (json.JSONDecodeError, OSError, IOError):
194
+ pass
195
+
196
+ return '[Setup incomplete — run /flydocs-setup to configure your project]'
180
197
  except (json.JSONDecodeError, OSError, IOError):
181
198
  pass
182
199
  return None
@@ -1 +1 @@
1
- 0.6.0-alpha.7
1
+ 0.6.0-alpha.8
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.7",
2
+ "version": "0.6.0-alpha.8",
3
3
  "description": "FlyDocs Core - Manifest of all managed files",
4
4
  "repository": "github.com/plastrlab/flydocs-core",
5
5