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.
- package/README.md +39 -14
- package/bin/atris.js +39 -15
- package/commands/business.js +514 -24
- package/commands/computer.js +326 -9
- package/commands/errors.js +155 -0
- package/commands/proof.js +115 -0
- package/commands/pull.js +12 -6
- package/commands/push.js +8 -2
- package/commands/task.js +217 -0
- package/commands/visualize.js +324 -8
- package/lib/task-db.js +288 -0
- package/lib/todo-fallback.js +142 -0
- package/lib/todo.js +99 -184
- package/package.json +2 -2
- package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
package/commands/business.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
1911
|
-
console.
|
|
1912
|
-
|
|
1913
|
-
|
|
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,
|