@flydocs/cli 0.6.0-alpha.6 → 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.6";
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";
@@ -50,6 +50,7 @@ async function ensureDirectories(targetDir, tier) {
50
50
  "flydocs/knowledge/decisions",
51
51
  "flydocs/knowledge/notes",
52
52
  "flydocs/knowledge/product",
53
+ "flydocs/knowledge/templates",
53
54
  "flydocs/design-system"
54
55
  ];
55
56
  if (tier === "local") {
@@ -292,10 +293,9 @@ function extractPreservedValues(config) {
292
293
  return {
293
294
  tier: config.tier,
294
295
  setupComplete: config.setupComplete ?? false,
295
- providerTeamId: config.provider?.teamId ?? null,
296
+ workspaceId: config.workspaceId ?? null,
296
297
  workspace: config.workspace ?? {},
297
298
  issueLabels: config.issueLabels ?? {},
298
- statusMapping: config.statusMapping ?? {},
299
299
  detectedStack: config.detectedStack ?? {},
300
300
  skills: config.skills ?? {},
301
301
  designSystem: config.designSystem ?? null,
@@ -311,21 +311,13 @@ async function mergeConfig(templateDir, version, tierFlag, preserved) {
311
311
  config.version = version;
312
312
  config.tier = tierFlag ?? preserved.tier;
313
313
  config.setupComplete = preserved.setupComplete;
314
- if (preserved.providerTeamId !== null) {
315
- if (!config.provider) {
316
- config.provider = { type: "linear", teamId: null };
317
- }
318
- config.provider.teamId = preserved.providerTeamId;
319
- }
314
+ config.workspaceId = preserved.workspaceId;
320
315
  if (Object.keys(preserved.workspace).length > 0) {
321
316
  config.workspace = preserved.workspace;
322
317
  }
323
318
  if (Object.keys(preserved.issueLabels).length > 0) {
324
319
  config.issueLabels = preserved.issueLabels;
325
320
  }
326
- if (Object.keys(preserved.statusMapping).length > 0) {
327
- config.statusMapping = preserved.statusMapping;
328
- }
329
321
  if (Object.keys(preserved.detectedStack).length > 0) {
330
322
  config.detectedStack = preserved.detectedStack;
331
323
  }
@@ -1887,6 +1879,21 @@ async function validateRelayKey(apiKey) {
1887
1879
  if (!data.valid) return { valid: false };
1888
1880
  return { valid: true, org: data.org ?? "your organization" };
1889
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
+ }
1890
1897
  async function validateLinearKey(apiKey) {
1891
1898
  const response = await fetch("https://api.linear.app/graphql", {
1892
1899
  method: "POST",
@@ -2073,6 +2080,8 @@ var init_install = __esm({
2073
2080
  }
2074
2081
  printInfo(`Tier: ${tier}`);
2075
2082
  await capture("install_tier_selected", { tier });
2083
+ let selectedWorkspaceId = null;
2084
+ let selectedWorkspaceName = null;
2076
2085
  if (tier === "cloud") {
2077
2086
  console.log();
2078
2087
  console.log(` ${pc6.bold("Connect to FlyDocs Cloud")}`);
@@ -2114,6 +2123,46 @@ var init_install = __esm({
2114
2123
  }
2115
2124
  const envFile = await storeEnvKey(targetDir, "FLYDOCS_API_KEY", apiKey);
2116
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
+ }
2117
2166
  }
2118
2167
  let skipConfigOverwrite = false;
2119
2168
  if (!await pathExists(join15(targetDir, ".git"))) {
@@ -2195,6 +2244,9 @@ var init_install = __esm({
2195
2244
  const configPath = join15(targetDir, ".flydocs", "config.json");
2196
2245
  if (!await pathExists(configPath)) {
2197
2246
  const config = await createFreshConfig(templateDir, version, tier);
2247
+ if (selectedWorkspaceId) {
2248
+ config.workspaceId = selectedWorkspaceId;
2249
+ }
2198
2250
  await writeConfig(targetDir, config);
2199
2251
  printStatus(`.flydocs/config.json (new, tier: ${tier})`);
2200
2252
  } else if (args.tier) {
@@ -2202,15 +2254,34 @@ var init_install = __esm({
2202
2254
  const existing = await readConfig(targetDir);
2203
2255
  existing.version = version;
2204
2256
  existing.tier = tier;
2257
+ if (selectedWorkspaceId) {
2258
+ existing.workspaceId = selectedWorkspaceId;
2259
+ }
2205
2260
  await writeConfig(targetDir, existing);
2206
2261
  printStatus(`.flydocs/config.json (tier updated: ${tier})`);
2207
2262
  } catch {
2208
2263
  const config = await createFreshConfig(templateDir, version, tier);
2264
+ if (selectedWorkspaceId) {
2265
+ config.workspaceId = selectedWorkspaceId;
2266
+ }
2209
2267
  await writeConfig(targetDir, config);
2210
2268
  printStatus(`.flydocs/config.json (recreated, tier: ${tier})`);
2211
2269
  }
2212
2270
  } else {
2213
- 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
+ }
2214
2285
  }
2215
2286
  console.log();
2216
2287
  console.log(`Installing skills (tier: ${tier})...`);
@@ -2357,6 +2428,45 @@ var init_install = __esm({
2357
2428
  ),
2358
2429
  label: "flydocs/knowledge/product/user-flows.md"
2359
2430
  },
2431
+ {
2432
+ src: join15(
2433
+ templateDir,
2434
+ "flydocs",
2435
+ "knowledge",
2436
+ "templates",
2437
+ "decision.md"
2438
+ ),
2439
+ dest: join15(
2440
+ targetDir,
2441
+ "flydocs",
2442
+ "knowledge",
2443
+ "templates",
2444
+ "decision.md"
2445
+ ),
2446
+ label: "flydocs/knowledge/templates/decision.md"
2447
+ },
2448
+ {
2449
+ src: join15(
2450
+ templateDir,
2451
+ "flydocs",
2452
+ "knowledge",
2453
+ "templates",
2454
+ "feature.md"
2455
+ ),
2456
+ dest: join15(
2457
+ targetDir,
2458
+ "flydocs",
2459
+ "knowledge",
2460
+ "templates",
2461
+ "feature.md"
2462
+ ),
2463
+ label: "flydocs/knowledge/templates/feature.md"
2464
+ },
2465
+ {
2466
+ src: join15(templateDir, "flydocs", "knowledge", "templates", "note.md"),
2467
+ dest: join15(targetDir, "flydocs", "knowledge", "templates", "note.md"),
2468
+ label: "flydocs/knowledge/templates/note.md"
2469
+ },
2360
2470
  {
2361
2471
  src: join15(templateDir, "flydocs", "design-system", "README.md"),
2362
2472
  dest: join15(targetDir, "flydocs", "design-system", "README.md"),
@@ -2770,10 +2880,9 @@ var init_update = __esm({
2770
2880
  let preserved = {
2771
2881
  tier: "cloud",
2772
2882
  setupComplete: false,
2773
- providerTeamId: null,
2883
+ workspaceId: null,
2774
2884
  workspace: {},
2775
2885
  issueLabels: {},
2776
- statusMapping: {},
2777
2886
  detectedStack: {},
2778
2887
  skills: {},
2779
2888
  designSystem: null,
@@ -2797,6 +2906,7 @@ var init_update = __esm({
2797
2906
  } else {
2798
2907
  effectiveTier = preserved.tier;
2799
2908
  }
2909
+ await ensureDirectories(targetDir, effectiveTier);
2800
2910
  console.log("Replacing framework directories...");
2801
2911
  await replaceDirectory(
2802
2912
  join16(templateDir, ".flydocs", "templates"),
@@ -2911,6 +3021,22 @@ var init_update = __esm({
2911
3021
  await copyFile(envExampleSrc, join16(targetDir, ".env.example"));
2912
3022
  printStatus(".env.example");
2913
3023
  }
3024
+ const knowledgeTemplatesDir = join16(
3025
+ targetDir,
3026
+ "flydocs",
3027
+ "knowledge",
3028
+ "templates"
3029
+ );
3030
+ if (!await pathExists(knowledgeTemplatesDir)) {
3031
+ await mkdir8(knowledgeTemplatesDir, { recursive: true });
3032
+ }
3033
+ for (const tmpl of ["decision.md", "feature.md", "note.md"]) {
3034
+ const src = join16(templateDir, "flydocs", "knowledge", "templates", tmpl);
3035
+ const dest = join16(knowledgeTemplatesDir, tmpl);
3036
+ if (await pathExists(src) && !await pathExists(dest)) {
3037
+ await copyFile(src, dest);
3038
+ }
3039
+ }
2914
3040
  await runManifestGeneration(targetDir);
2915
3041
  await runContextGraphBuild(targetDir);
2916
3042
  console.log();
@@ -3616,7 +3742,9 @@ var init_connect = __esm({
3616
3742
  }
3617
3743
  const wasLocal = config.tier === "local";
3618
3744
  config.tier = "cloud";
3619
- config.provider = config.provider ?? { type: null, teamId: null };
3745
+ const configRecord = config;
3746
+ delete configRecord.statusMapping;
3747
+ delete configRecord.provider;
3620
3748
  await writeConfig(targetDir, config);
3621
3749
  printStatus("Config updated to cloud tier");
3622
3750
  if (wasLocal) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flydocs/cli",
3
- "version": "0.6.0-alpha.6",
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.6",
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.6
1
+ 0.6.0-alpha.8
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.6",
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