atris 3.12.1 → 3.14.0

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.
@@ -13,16 +13,312 @@ function getBusinessConfigPath() {
13
13
  return path.join(dir, 'businesses.json');
14
14
  }
15
15
 
16
+ function slugify(name) {
17
+ return String(name || '')
18
+ .toLowerCase()
19
+ .trim()
20
+ .replace(/[^a-z0-9\s-]/g, '')
21
+ .replace(/\s+/g, '-')
22
+ .replace(/-+/g, '-')
23
+ .replace(/^-+|-+$/g, '');
24
+ }
25
+
26
+ function isHelpToken(arg) {
27
+ return arg === '--help' || arg === '-h' || arg === 'help' || arg === '-?';
28
+ }
29
+
16
30
  function loadBusinesses() {
17
31
  const p = getBusinessConfigPath();
18
32
  if (!fs.existsSync(p)) return {};
19
33
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
20
34
  }
21
35
 
36
+ function businessAliases(business) {
37
+ const aliases = business?.aliases || business?.config?.aliases || [];
38
+ return Array.isArray(aliases) ? aliases : [];
39
+ }
40
+
41
+ function businessMatchesSlug(business, slug, { includeName = false } = {}) {
42
+ if (!business || !slug) return false;
43
+ const wanted = String(slug).toLowerCase();
44
+ const canonical = String(business.slug || '').toLowerCase();
45
+ const aliases = businessAliases(business).map((alias) => String(alias).toLowerCase());
46
+ if (canonical === wanted || aliases.includes(wanted)) return true;
47
+ return includeName && String(business.name || '').toLowerCase() === wanted;
48
+ }
49
+
22
50
  function saveBusinesses(data) {
23
51
  fs.writeFileSync(getBusinessConfigPath(), JSON.stringify(data, null, 2));
24
52
  }
25
53
 
54
+ function buildBusinessCacheEntry(business, localSlug, existing = {}) {
55
+ const aliases = businessAliases(business);
56
+ const entry = {
57
+ business_id: business.id || business.business_id,
58
+ workspace_id: business.workspace_id,
59
+ name: business.name || localSlug,
60
+ slug: localSlug || business.slug,
61
+ added_at: existing.added_at || new Date().toISOString(),
62
+ };
63
+ if (business.slug && business.slug !== entry.slug) entry.canonical_slug = business.slug;
64
+ else if (business.slug) entry.canonical_slug = business.slug;
65
+ if (aliases.length > 0) entry.aliases = aliases;
66
+ return entry;
67
+ }
68
+
69
+ function readBusinessFolderBindings(rootDir = path.join(os.homedir(), 'arena', 'atris-business')) {
70
+ const skip = new Set([
71
+ '.git', '.DS_Store', 'archive', 'archives', '_archive', 'bench', 'deals',
72
+ 'node_modules', 'shelf', '_shelf', 'templates',
73
+ ]);
74
+ if (!fs.existsSync(rootDir)) return [];
75
+
76
+ return fs.readdirSync(rootDir, { withFileTypes: true })
77
+ .filter((entry) => !entry.name.startsWith('.') && !skip.has(entry.name))
78
+ .map((entry) => {
79
+ const fullPath = path.join(rootDir, entry.name);
80
+ let isDirectory = entry.isDirectory();
81
+ const isSymlink = entry.isSymbolicLink();
82
+ let symlinkTarget = null;
83
+ if (isSymlink) {
84
+ try {
85
+ symlinkTarget = fs.readlinkSync(fullPath);
86
+ isDirectory = fs.statSync(fullPath).isDirectory();
87
+ } catch {
88
+ isDirectory = false;
89
+ }
90
+ }
91
+ if (!isDirectory) return null;
92
+
93
+ const businessJsonPath = path.join(fullPath, '.atris', 'business.json');
94
+ const atrisDir = path.join(fullPath, 'atris');
95
+ const binding = {
96
+ name: entry.name,
97
+ path: fullPath,
98
+ isSymlink,
99
+ symlinkTarget,
100
+ hasAtris: fs.existsSync(atrisDir) && fs.statSync(atrisDir).isDirectory(),
101
+ hasBusinessJson: fs.existsSync(businessJsonPath),
102
+ businessJsonPath,
103
+ meta: null,
104
+ error: null,
105
+ };
106
+
107
+ if (binding.hasBusinessJson) {
108
+ try {
109
+ binding.meta = JSON.parse(fs.readFileSync(businessJsonPath, 'utf8'));
110
+ } catch (err) {
111
+ binding.error = err.message || 'invalid JSON';
112
+ }
113
+ }
114
+ return binding;
115
+ })
116
+ .filter(Boolean);
117
+ }
118
+
119
+ function analyzeBusinessDoctor({ cache = {}, cloudBusinesses = [], folderBindings = [] } = {}) {
120
+ const issues = [];
121
+ const cacheUpdates = {};
122
+ const activeById = new Map();
123
+ const activeBySlug = new Map();
124
+ const realFolderIds = new Map();
125
+
126
+ for (const business of cloudBusinesses || []) {
127
+ const id = business.id || business.business_id;
128
+ if (!id) continue;
129
+ activeById.set(id, business);
130
+ if (business.slug) {
131
+ const lower = String(business.slug).toLowerCase();
132
+ if (activeBySlug.has(lower)) {
133
+ issues.push({
134
+ level: 'fail',
135
+ code: 'duplicate-active-slug',
136
+ subject: business.slug,
137
+ message: `multiple active cloud businesses use slug ${business.slug}`,
138
+ });
139
+ }
140
+ activeBySlug.set(lower, business);
141
+ }
142
+ }
143
+
144
+ const findActiveMatch = (key, entry = {}) => {
145
+ if (entry.business_id && activeById.has(entry.business_id)) return activeById.get(entry.business_id);
146
+ const candidates = [key, entry.slug, entry.canonical_slug, entry.name].filter(Boolean);
147
+ return cloudBusinesses.find((business) =>
148
+ candidates.some((candidate) => businessMatchesSlug(business, candidate, { includeName: true }))
149
+ ) || null;
150
+ };
151
+
152
+ for (const [key, entry] of Object.entries(cache || {})) {
153
+ const active = findActiveMatch(key, entry);
154
+ if (!active) {
155
+ issues.push({
156
+ level: 'fail',
157
+ code: 'stale-cache',
158
+ subject: key,
159
+ message: `${key} points at a deleted/inaccessible business (${entry.business_id || 'missing id'})`,
160
+ });
161
+ continue;
162
+ }
163
+
164
+ if (entry.business_id && entry.business_id !== active.id) {
165
+ issues.push({
166
+ level: 'fail',
167
+ code: 'stale-cache-repoint',
168
+ subject: key,
169
+ message: `${key} points at ${entry.business_id}; active cloud row is ${active.id}`,
170
+ fixable: true,
171
+ });
172
+ cacheUpdates[key] = buildBusinessCacheEntry(active, key, entry);
173
+ }
174
+
175
+ if (!businessMatchesSlug({ ...active, aliases: businessAliases(active) }, key, { includeName: true }) && key !== active.slug) {
176
+ issues.push({
177
+ level: 'warn',
178
+ code: 'cache-key-not-alias',
179
+ subject: key,
180
+ message: `${key} is cached but is not the canonical slug, alias, or name for ${active.slug}`,
181
+ });
182
+ }
183
+ }
184
+
185
+ for (const business of cloudBusinesses || []) {
186
+ const canonicalKey = business.slug;
187
+ if (canonicalKey && (!cache[canonicalKey] || cache[canonicalKey].business_id !== business.id)) {
188
+ issues.push({
189
+ level: 'warn',
190
+ code: 'missing-canonical-cache',
191
+ subject: canonicalKey,
192
+ message: `${canonicalKey} is active in cloud but missing/stale in local cache`,
193
+ fixable: true,
194
+ });
195
+ cacheUpdates[canonicalKey] = buildBusinessCacheEntry(business, canonicalKey, cache[canonicalKey]);
196
+ }
197
+
198
+ for (const alias of businessAliases(business)) {
199
+ if (!cache[alias] || cache[alias].business_id !== business.id) {
200
+ issues.push({
201
+ level: 'warn',
202
+ code: 'missing-alias-cache',
203
+ subject: alias,
204
+ message: `${alias} is an active alias for ${business.slug} but missing/stale in local cache`,
205
+ fixable: true,
206
+ });
207
+ cacheUpdates[alias] = buildBusinessCacheEntry(business, alias, cache[alias]);
208
+ }
209
+ }
210
+ }
211
+
212
+ for (const binding of folderBindings || []) {
213
+ if (binding.error) {
214
+ issues.push({
215
+ level: 'fail',
216
+ code: 'invalid-business-json',
217
+ subject: binding.name,
218
+ message: `${binding.name}/.atris/business.json is invalid: ${binding.error}`,
219
+ });
220
+ continue;
221
+ }
222
+
223
+ if (!binding.hasBusinessJson) {
224
+ if (binding.hasAtris && !binding.isSymlink) {
225
+ issues.push({
226
+ level: 'warn',
227
+ code: 'folder-unbound',
228
+ subject: binding.name,
229
+ message: `${binding.name} has atris/ but no .atris/business.json`,
230
+ });
231
+ }
232
+ continue;
233
+ }
234
+
235
+ const meta = binding.meta || {};
236
+ const active = findActiveMatch(binding.name, {
237
+ business_id: meta.business_id,
238
+ slug: meta.slug,
239
+ canonical_slug: meta.canonical_slug,
240
+ name: meta.name,
241
+ });
242
+
243
+ if (!active) {
244
+ issues.push({
245
+ level: 'fail',
246
+ code: 'stale-folder-binding',
247
+ subject: binding.name,
248
+ message: `${binding.name} is bound to deleted/inaccessible business ${meta.business_id || meta.slug || 'unknown'}`,
249
+ });
250
+ continue;
251
+ }
252
+
253
+ if (meta.business_id && meta.business_id !== active.id) {
254
+ issues.push({
255
+ level: 'fail',
256
+ code: 'folder-id-mismatch',
257
+ subject: binding.name,
258
+ message: `${binding.name} points at ${meta.business_id}; active cloud row is ${active.id}`,
259
+ });
260
+ }
261
+
262
+ if (meta.slug && !businessMatchesSlug(active, meta.slug, { includeName: true })) {
263
+ issues.push({
264
+ level: 'fail',
265
+ code: 'folder-slug-mismatch',
266
+ subject: binding.name,
267
+ message: `${binding.name} uses slug ${meta.slug}, which is not ${active.slug} or an alias`,
268
+ });
269
+ }
270
+
271
+ if (!businessMatchesSlug(active, binding.name, { includeName: false }) && !binding.isSymlink) {
272
+ issues.push({
273
+ level: 'warn',
274
+ code: 'folder-name-not-slug-or-alias',
275
+ subject: binding.name,
276
+ message: `${binding.name} is not a canonical slug or alias for ${active.slug}`,
277
+ });
278
+ }
279
+
280
+ if (!binding.isSymlink) {
281
+ const existing = realFolderIds.get(active.id) || [];
282
+ existing.push(binding.name);
283
+ realFolderIds.set(active.id, existing);
284
+ }
285
+
286
+ const cacheKey = meta.slug || binding.name;
287
+ if (!cache[cacheKey] || cache[cacheKey].business_id !== active.id) {
288
+ issues.push({
289
+ level: 'warn',
290
+ code: 'folder-cache-missing',
291
+ subject: cacheKey,
292
+ message: `${binding.name} is bound locally but ${cacheKey} is missing/stale in local cache`,
293
+ fixable: true,
294
+ });
295
+ cacheUpdates[cacheKey] = buildBusinessCacheEntry(active, cacheKey, cache[cacheKey]);
296
+ }
297
+ }
298
+
299
+ for (const [businessId, names] of realFolderIds.entries()) {
300
+ if (names.length > 1) {
301
+ issues.push({
302
+ level: 'fail',
303
+ code: 'duplicate-real-folders',
304
+ subject: businessId,
305
+ message: `business ${businessId} has multiple real folders: ${names.join(', ')}`,
306
+ });
307
+ }
308
+ }
309
+
310
+ return {
311
+ issues,
312
+ cacheUpdates,
313
+ stats: {
314
+ cache_entries: Object.keys(cache || {}).length,
315
+ cloud_active: cloudBusinesses.length,
316
+ folders: folderBindings.length,
317
+ fixable_cache_entries: Object.keys(cacheUpdates).length,
318
+ },
319
+ };
320
+ }
321
+
26
322
  function parseCreateBusinessFlags(flags, cwd = process.cwd()) {
27
323
  const options = {
28
324
  description: '',
@@ -774,7 +1070,45 @@ function detectBusinessSlug(explicitSlug) {
774
1070
  }
775
1071
  }
776
1072
 
1073
+ async function findExistingBusinessBySlug(slug, token) {
1074
+ if (!slug) return null;
1075
+
1076
+ // Local cache first — no network round-trip needed.
1077
+ const local = loadBusinesses();
1078
+ if (local[slug]) {
1079
+ return { id: local[slug].business_id, name: local[slug].name, slug, source: 'local' };
1080
+ }
1081
+ for (const v of Object.values(local)) {
1082
+ if (businessMatchesSlug(v, slug)) {
1083
+ return { id: v.business_id, name: v.name, slug, source: 'local' };
1084
+ }
1085
+ }
1086
+
1087
+ if (!token) return null;
1088
+
1089
+ // Cloud lookup — covers businesses the user is a member of but hasn't added.
1090
+ const direct = await apiRequestJson(`/business/by-slug/${encodeURIComponent(slug)}`, {
1091
+ method: 'GET',
1092
+ token,
1093
+ });
1094
+ if (direct.ok && direct.data && direct.data.id) {
1095
+ return { id: direct.data.id, name: direct.data.name, slug: direct.data.slug || slug, source: 'cloud' };
1096
+ }
1097
+
1098
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
1099
+ if (list.ok && Array.isArray(list.data)) {
1100
+ const match = list.data.find(b => businessMatchesSlug(b, slug));
1101
+ if (match) return { id: match.id, name: match.name, slug: match.slug, source: 'cloud' };
1102
+ }
1103
+
1104
+ return null;
1105
+ }
1106
+
777
1107
  async function addBusiness(slug) {
1108
+ if (!slug || isHelpToken(slug)) {
1109
+ console.error('Usage: atris business add <slug>');
1110
+ process.exit(1);
1111
+ }
778
1112
  if (!slug) {
779
1113
  console.error('Usage: atris business add <slug>');
780
1114
  process.exit(1);
@@ -796,7 +1130,7 @@ async function addBusiness(slug) {
796
1130
  // Try listing all and matching
797
1131
  const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
798
1132
  if (listResult.ok && Array.isArray(listResult.data)) {
799
- const match = listResult.data.find(b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase());
1133
+ const match = listResult.data.find(b => businessMatchesSlug(b, slug, { includeName: true }));
800
1134
  if (match) {
801
1135
  const businesses = loadBusinesses();
802
1136
  businesses[slug] = {
@@ -1008,7 +1342,7 @@ function listBusinessesLocal(opts = {}) {
1008
1342
  }
1009
1343
 
1010
1344
  async function removeBusiness(slug) {
1011
- if (!slug) {
1345
+ if (!slug || isHelpToken(slug)) {
1012
1346
  console.error('Usage: atris business remove <slug>');
1013
1347
  process.exit(1);
1014
1348
  }
@@ -1047,7 +1381,7 @@ async function resolveSlug(slug, creds) {
1047
1381
  // Fallback: list all and match
1048
1382
  const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
1049
1383
  if (listResult.ok && Array.isArray(listResult.data)) {
1050
- const match = listResult.data.find(b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase());
1384
+ const match = listResult.data.find(b => businessMatchesSlug(b, slug, { includeName: true }));
1051
1385
  if (match) {
1052
1386
  return { business_id: match.id, workspace_id: match.workspace_id, name: match.name, slug: match.slug };
1053
1387
  }
@@ -1261,9 +1595,109 @@ async function businessAudit() {
1261
1595
  console.log('');
1262
1596
  }
1263
1597
 
1598
+ function parseBusinessDoctorOptions(args = []) {
1599
+ const options = {
1600
+ fix: args.includes('--fix'),
1601
+ json: args.includes('--json'),
1602
+ root: path.join(os.homedir(), 'arena', 'atris-business'),
1603
+ };
1604
+ const rootIdx = args.indexOf('--root');
1605
+ if (rootIdx !== -1 && args[rootIdx + 1]) {
1606
+ options.root = path.resolve(args[rootIdx + 1]);
1607
+ }
1608
+ return options;
1609
+ }
1610
+
1611
+ function printBusinessDoctorHelp() {
1612
+ console.log('Usage: atris business doctor [--fix] [--root <dir>] [--json]');
1613
+ console.log('');
1614
+ console.log('Checks cloud-active businesses against:');
1615
+ console.log(' - ~/.atris/businesses.json');
1616
+ console.log(' - ~/arena/atris-business/*/.atris/business.json');
1617
+ console.log(' - canonical slug + alias bindings');
1618
+ console.log('');
1619
+ console.log('--fix rewrites only safe local cache entries. It does not rename folders or touch cloud data.');
1620
+ }
1621
+
1622
+ async function businessDoctor(...args) {
1623
+ if (args.some(isHelpToken)) {
1624
+ printBusinessDoctorHelp();
1625
+ return;
1626
+ }
1627
+
1628
+ const options = parseBusinessDoctorOptions(args);
1629
+ const creds = loadCredentials();
1630
+ if (!creds || !creds.token) {
1631
+ console.error('Not logged in. Run: atris login');
1632
+ process.exit(1);
1633
+ }
1634
+
1635
+ const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
1636
+ if (!listResult.ok || !Array.isArray(listResult.data)) {
1637
+ console.error(`Failed to fetch businesses: ${listResult.errorMessage || listResult.error || listResult.status || 'unknown error'}`);
1638
+ process.exit(1);
1639
+ }
1640
+
1641
+ let cache = loadBusinesses();
1642
+ const folderBindings = readBusinessFolderBindings(options.root);
1643
+ let analysis = analyzeBusinessDoctor({
1644
+ cache,
1645
+ cloudBusinesses: listResult.data,
1646
+ folderBindings,
1647
+ });
1648
+
1649
+ const cacheUpdateKeys = Object.keys(analysis.cacheUpdates);
1650
+ let fixed = [];
1651
+ if (options.fix && cacheUpdateKeys.length > 0) {
1652
+ cache = { ...cache, ...analysis.cacheUpdates };
1653
+ saveBusinesses(cache);
1654
+ fixed = cacheUpdateKeys;
1655
+ analysis = analyzeBusinessDoctor({
1656
+ cache,
1657
+ cloudBusinesses: listResult.data,
1658
+ folderBindings,
1659
+ });
1660
+ }
1661
+
1662
+ if (options.json) {
1663
+ console.log(JSON.stringify({
1664
+ root: options.root,
1665
+ stats: analysis.stats,
1666
+ fixed,
1667
+ issues: analysis.issues,
1668
+ }, null, 2));
1669
+ } else {
1670
+ console.log('');
1671
+ console.log('Business Doctor');
1672
+ console.log('---------------');
1673
+ console.log(`cloud active: ${analysis.stats.cloud_active}`);
1674
+ console.log(`cache entries: ${analysis.stats.cache_entries}`);
1675
+ console.log(`folders scanned: ${analysis.stats.folders}`);
1676
+ if (fixed.length > 0) console.log(`fixed cache entries: ${fixed.join(', ')}`);
1677
+ console.log('');
1678
+
1679
+ if (analysis.issues.length === 0) {
1680
+ console.log('OK no business binding drift found.');
1681
+ } else {
1682
+ for (const issue of analysis.issues) {
1683
+ const label = issue.level === 'fail' ? 'FAIL' : 'WARN';
1684
+ const fixHint = issue.fixable ? ' (run with --fix)' : '';
1685
+ console.log(`${label} ${issue.code}: ${issue.message}${fixHint}`);
1686
+ }
1687
+ }
1688
+ console.log('');
1689
+ }
1690
+
1691
+ const failures = analysis.issues.filter((issue) => issue.level === 'fail');
1692
+ if (failures.length > 0) process.exitCode = 1;
1693
+ }
1694
+
1264
1695
  async function createBusinessInternal(name, flags = [], mode = 'auto') {
1265
- if (!name) {
1696
+ if (!name || isHelpToken(name) || String(name).startsWith('-')) {
1266
1697
  console.error('Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]');
1698
+ if (name && String(name).startsWith('-') && !isHelpToken(name)) {
1699
+ console.error(`\n Refusing to create a business named "${name}" — looks like a flag, not a name.`);
1700
+ }
1267
1701
  process.exit(1);
1268
1702
  }
1269
1703
 
@@ -1275,6 +1709,29 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
1275
1709
 
1276
1710
  const options = parseCreateBusinessFlags(flags);
1277
1711
  const description = options.description;
1712
+ const force = flags.includes('--force') || flags.includes('--allow-duplicate');
1713
+
1714
+ // Pre-flight: refuse to create a duplicate by slug. The backend will silently
1715
+ // suffix `-1`, `-2`, etc., which produces ghost businesses when users actually
1716
+ // wanted to attach to an existing one. Guide them to `atris pull` instead.
1717
+ if (!force) {
1718
+ const desiredSlug = slugify(name);
1719
+ if (desiredSlug) {
1720
+ const existing = await findExistingBusinessBySlug(desiredSlug, creds.token);
1721
+ if (existing) {
1722
+ console.error(`\nA business with slug "${desiredSlug}" already exists.`);
1723
+ console.error(` Name: ${existing.name || desiredSlug}`);
1724
+ if (existing.id) console.error(` ID: ${existing.id}`);
1725
+ console.error('');
1726
+ console.error('To set up a local workspace for it, run:');
1727
+ console.error(` atris pull ${desiredSlug} # into ./${desiredSlug}`);
1728
+ console.error(` atris pull ${desiredSlug} --into <path> # into a custom path`);
1729
+ console.error('');
1730
+ console.error(`To create a NEW business anyway (will be slugged "${desiredSlug}-1"), pass --force.`);
1731
+ process.exit(1);
1732
+ }
1733
+ }
1734
+ }
1278
1735
 
1279
1736
  console.log(`Creating business: ${name}...`);
1280
1737
 
@@ -1700,7 +2157,7 @@ async function deployBusiness(slug) {
1700
2157
  // Try to find by slug in cloud
1701
2158
  const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
1702
2159
  if (listResult.ok && Array.isArray(listResult.data)) {
1703
- const match = listResult.data.find(b => b.slug === slug);
2160
+ const match = listResult.data.find(b => businessMatchesSlug(b, slug));
1704
2161
  if (match) {
1705
2162
  bizConfig = { business_id: match.id, workspace_id: match.workspace_id, name: match.name, slug: match.slug };
1706
2163
  businesses[slug] = { ...bizConfig, added_at: new Date().toISOString() };
@@ -1832,12 +2289,53 @@ async function quickstart() {
1832
2289
  (get 1 email/day instead of every notification)
1833
2290
 
1834
2291
  Templates: saas, agency, ecommerce, content, restaurant
2292
+
2293
+ Rule of thumb:
2294
+ atris business init "<name>" = cloud + local business computer workspace
2295
+ atris business create "<name>" = cloud-only unless you pass --workspace
1835
2296
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1836
2297
  `);
1837
2298
  }
1838
2299
 
1839
2300
 
2301
+ function printBusinessHelp() {
2302
+ console.log('Usage: atris business <command> [args]');
2303
+ console.log('');
2304
+ console.log(' quickstart ← Start here! 3-command guide');
2305
+ console.log('');
2306
+ console.log(' init <name> RECOMMENDED: create a business environment (cloud + local)');
2307
+ console.log(' workspace <name> Alias for init');
2308
+ console.log(' create <name> Cloud-only business record; add --workspace to also scaffold local');
2309
+ console.log(' add <slug> Register an existing cloud business');
2310
+ console.log(' list Show registered businesses');
2311
+ console.log(' team [slug] Show members, roles, and admin access');
2312
+ console.log(' status <slug> Quick status check');
2313
+ console.log(' health [slug] Full health dashboard');
2314
+ console.log(' audit Audit all businesses');
2315
+ console.log(' doctor [--fix] Find stale business cache, alias, and folder bindings');
2316
+ console.log(' connect <service> Connect a skill/integration');
2317
+ console.log(' notify <mode> Set notification mode (digest/silent/push)');
2318
+ console.log(' deploy <slug> Push local business to cloud');
2319
+ console.log(' onboard Seed brief, person, first loop, safe next action, and one-pager from sparse input');
2320
+ console.log(' record <report> Append recap state into events, episodes, and scorecards');
2321
+ console.log(' remove <slug> Unregister locally');
2322
+ console.log('');
2323
+ console.log(' Already-attached business? Run `atris pull <slug>` to scaffold a local workspace.');
2324
+ }
2325
+
1840
2326
  async function businessCommand(subcommand, ...args) {
2327
+ // Help intercept — without this, `atris business init --help` would treat
2328
+ // `--help` as a business name and create one. Same for any subcommand that
2329
+ // takes a positional name/slug.
2330
+ if (!subcommand || isHelpToken(subcommand)) {
2331
+ printBusinessHelp();
2332
+ return;
2333
+ }
2334
+ if (args.length > 0 && isHelpToken(args[0]) && subcommand !== 'doctor') {
2335
+ printBusinessHelp();
2336
+ return;
2337
+ }
2338
+
1841
2339
  switch (subcommand) {
1842
2340
  case 'add':
1843
2341
  await addBusiness(args[0]);
@@ -1883,6 +2381,9 @@ async function businessCommand(subcommand, ...args) {
1883
2381
  case 'audit':
1884
2382
  await businessAudit();
1885
2383
  break;
2384
+ case 'doctor':
2385
+ await businessDoctor(...args);
2386
+ break;
1886
2387
  case 'connect':
1887
2388
  await connectService(args[0], ...args.slice(1));
1888
2389
  break;
@@ -1907,25 +2408,10 @@ async function businessCommand(subcommand, ...args) {
1907
2408
  await quickstart();
1908
2409
  break;
1909
2410
  default:
1910
- console.log('Usage: atris business <command> [args]');
1911
- console.log('');
1912
- console.log(' quickstart ← Start here! 3-command guide');
1913
- console.log('');
1914
- console.log(' init <name> Create a business environment (cloud + local)');
1915
- console.log(' workspace <name> Alias for init');
1916
- console.log(' create <name> Create the cloud business; add --workspace for a local business environment');
1917
- console.log(' add <slug> Register an existing cloud business');
1918
- console.log(' list Show registered businesses');
1919
- console.log(' team [slug] Show members, roles, and admin access');
1920
- console.log(' status <slug> Quick status check');
1921
- console.log(' health [slug] Full health dashboard');
1922
- console.log(' audit Audit all businesses');
1923
- console.log(' connect <service> Connect a skill/integration');
1924
- console.log(' notify <mode> Set notification mode (digest/silent/push)');
1925
- console.log(' deploy <slug> Push local business to cloud');
1926
- console.log(' onboard Seed brief, person, first loop, safe next action, and one-pager from sparse input');
1927
- console.log(' record <report> Append recap state into events, episodes, and scorecards');
1928
- console.log(' remove <slug> Unregister locally');
2411
+ console.error(`Unknown subcommand: ${subcommand}`);
2412
+ console.error('');
2413
+ printBusinessHelp();
2414
+ process.exitCode = 1;
1929
2415
  }
1930
2416
  }
1931
2417
 
@@ -1933,10 +2419,14 @@ module.exports = {
1933
2419
  businessCommand,
1934
2420
  businessHealth,
1935
2421
  businessAudit,
2422
+ businessDoctor,
1936
2423
  businessTeam,
1937
2424
  loadBusinesses,
1938
2425
  saveBusinesses,
1939
2426
  getBusinessConfigPath,
2427
+ businessMatchesSlug,
2428
+ analyzeBusinessDoctor,
2429
+ readBusinessFolderBindings,
1940
2430
  createCanonicalBusinessWorkspace,
1941
2431
  initBusinessWorkspace,
1942
2432
  onboardBusiness,