@descix/cli 1.0.16 → 1.0.18
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/agent-assets/guides/README.md +2 -1
- package/agent-assets/guides/dynamic-site-pattern.md +6 -10
- package/agent-assets/guides/knowledge-only-pattern.md +12 -11
- package/agent-assets/guides/microservice-pattern.md +13 -16
- package/agent-assets/guides/static-site-pattern.md +6 -7
- package/agent-assets/instructions/app-asset-validation.md +61 -59
- package/agent-assets/instructions/onboarding-flow.md +57 -73
- package/agent-assets/instructions/standard-folder-structure.md +39 -36
- package/agent-assets/reference/app-creation.md +27 -33
- package/agent-assets/reference/architecture.md +85 -148
- package/agent-assets/reference/local-dev.md +5 -83
- package/agent-assets/reference/mcp-integration.md +32 -39
- package/agent-assets/reference/workspace-config.md +9 -10
- package/bin/descix.js +30 -133
- package/lib/commands/appInit.js +154 -0
- package/lib/commands/init.js +14 -54
- package/lib/help-guard.js +85 -0
- package/lib/verb-invokes.generated.json +5 -1
- package/package.json +1 -1
- package/templates/agent-claude.md +14 -9
- package/templates/agent-cline.md +7 -7
- package/templates/agent-copilot.md +7 -7
- package/templates/agent-cursor.md +7 -7
|
@@ -10,7 +10,7 @@ This document covers how AI agents interact with the DeSciX platform via MCP (Mo
|
|
|
10
10
|
|
|
11
11
|
## 1. Architecture Overview
|
|
12
12
|
|
|
13
|
-
DeSciX operates as a **Federated MCP Service Mesh**. The
|
|
13
|
+
DeSciX operates as a **Federated MCP Service Mesh**. The DeSciX backend acts as a broker that aggregates tools from all registered microservices into a unified catalog.
|
|
14
14
|
|
|
15
15
|
**Key Innovation:** AI agents discover and invoke capabilities across the entire mesh without knowing where individual services are hosted.
|
|
16
16
|
|
|
@@ -24,7 +24,7 @@ AI Agent (Cursor/MCP Client)
|
|
|
24
24
|
execute_remote_command
|
|
25
25
|
│
|
|
26
26
|
▼
|
|
27
|
-
|
|
27
|
+
DeSciX Backend (Federated Broker)
|
|
28
28
|
│
|
|
29
29
|
├──► Service 1 (Powch)
|
|
30
30
|
├──► Service 2 (SmartModel)
|
|
@@ -42,9 +42,9 @@ MCP provides the communication protocol between AI agents and the DeSciX platfor
|
|
|
42
42
|
**Key Tools:**
|
|
43
43
|
- `tell_me_how` - Semantic tool discovery
|
|
44
44
|
- `execute_remote_command` - Execute any backend command
|
|
45
|
-
- `
|
|
46
|
-
- `
|
|
47
|
-
- `
|
|
45
|
+
- `descix_doctor` - CLI-local startup diagnostic (auth, workspace, tools, warnings)
|
|
46
|
+
- `ask_question_to_app` - RAG chat with an app's knowledge base (synthesized answer, cited)
|
|
47
|
+
- `query_knowledge_base` - Raw vector search, returns chunks
|
|
48
48
|
|
|
49
49
|
### 2.2 Cursor/AI Agent Integration
|
|
50
50
|
|
|
@@ -91,13 +91,17 @@ Once signed in, `tell_me_how` is the **primary entry point** for discovering pla
|
|
|
91
91
|
|
|
92
92
|
**Rule:** Sign in, then ask `tell_me_how` first, then execute the recommended command.
|
|
93
93
|
|
|
94
|
-
### 3.2
|
|
94
|
+
### 3.2 Scopes
|
|
95
|
+
|
|
96
|
+
The validated `tell_me_how` scope enum has five values:
|
|
95
97
|
|
|
96
98
|
| Scope | Description | Use Case |
|
|
97
99
|
|-------|-------------|----------|
|
|
98
|
-
| `entitlements` (default) | Only tools user has access to | Production usage |
|
|
100
|
+
| `entitlements` (default) | Only tools the user has access to | Production usage |
|
|
99
101
|
| `project` | Tools filtered by workspace.json | Working within a project |
|
|
100
102
|
| `discovery` | All available tools | Exploring capabilities |
|
|
103
|
+
| `bootstrap` | Deterministic first-call on-ramp: platform summary, caller context, credit balance, essential tool schemas (no vector search; `question` is optional for this scope only) | First contact / first call in a session |
|
|
104
|
+
| `artifact` | Deterministic build/reproduce provenance for a published app (npm package + spec, artifact/notebook URLs, runnable `npx` commands), no vector search | "How do I build on / reproduce this app?" |
|
|
101
105
|
|
|
102
106
|
### 3.3 CLI Usage
|
|
103
107
|
|
|
@@ -151,7 +155,7 @@ After `descix quickstart` completes:
|
|
|
151
155
|
|
|
152
156
|
### 4.1 Federated MCP Broker
|
|
153
157
|
|
|
154
|
-
|
|
158
|
+
The DeSciX backend aggregates tools from all registered microservices:
|
|
155
159
|
|
|
156
160
|
```
|
|
157
161
|
Microservice A
|
|
@@ -161,7 +165,7 @@ Microservice A
|
|
|
161
165
|
Registers with Core
|
|
162
166
|
│
|
|
163
167
|
▼
|
|
164
|
-
|
|
168
|
+
DeSciX Backend (Broker)
|
|
165
169
|
└── Aggregated tools/list
|
|
166
170
|
│
|
|
167
171
|
▼
|
|
@@ -199,14 +203,11 @@ execute_remote_command({
|
|
|
199
203
|
|
|
200
204
|
### 4.3 Service Registration
|
|
201
205
|
|
|
202
|
-
Services self-register with the broker
|
|
206
|
+
Services self-register with the broker in one step — registration also vectorizes the README you
|
|
207
|
+
pass, so there is no separate developer-facing vectorize step:
|
|
203
208
|
|
|
204
209
|
```bash
|
|
205
|
-
|
|
206
|
-
descix microservice register -m ./manifest.json
|
|
207
|
-
|
|
208
|
-
# Vectorize SERVICE_README for discovery
|
|
209
|
-
descix microservice vectorize -r SERVICE_README.md
|
|
210
|
+
descix microservice register -r SERVICE_README.md
|
|
210
211
|
```
|
|
211
212
|
|
|
212
213
|
### 4.4 The execute_remote_command Pattern
|
|
@@ -309,11 +310,13 @@ When `--scope project` is used:
|
|
|
309
310
|
|
|
310
311
|
### 6.1 KB Operations via tell_me_how
|
|
311
312
|
|
|
312
|
-
|
|
313
|
+
`tell_me_how` can be asked about KB sync in plain language, but the actionable answer for a
|
|
314
|
+
developer is always the CLI verb, not the raw backend command names (`kb_sync_chunks` etc. are
|
|
315
|
+
internal to the backend, not something the CLI calls directly by name):
|
|
313
316
|
|
|
314
317
|
```bash
|
|
315
318
|
descix tell-me-how "How do I sync my knowledge base?"
|
|
316
|
-
#
|
|
319
|
+
# The developer-facing command either way: descix kb corpus sync
|
|
317
320
|
```
|
|
318
321
|
|
|
319
322
|
### 6.2 workspace.json from Hydration
|
|
@@ -324,7 +327,7 @@ descix tell-me-how "How do I sync my knowledge base?"
|
|
|
324
327
|
{
|
|
325
328
|
"env": { "apiUrl": "https://dev.descix.net", "products": [ /* one entry per app */ ] },
|
|
326
329
|
"driveConfig": {
|
|
327
|
-
"base_folder_id": "..." //
|
|
330
|
+
"base_folder_id": "..." // Only used by `descix drive pull/push` — unrelated to tell_me_how scope
|
|
328
331
|
}
|
|
329
332
|
}
|
|
330
333
|
```
|
|
@@ -334,9 +337,9 @@ descix tell-me-how "How do I sync my knowledge base?"
|
|
|
334
337
|
Custom services integrate with the same pattern:
|
|
335
338
|
|
|
336
339
|
1. Create `SERVICE_README_{name}.md`
|
|
337
|
-
2. Register with `descix microservice register`
|
|
338
|
-
|
|
339
|
-
|
|
340
|
+
2. Register with `descix microservice register -r SERVICE_README_{name}.md` — this both publishes
|
|
341
|
+
the manifest AND vectorizes the README in one step
|
|
342
|
+
3. Tools become discoverable via `tell_me_how`
|
|
340
343
|
|
|
341
344
|
---
|
|
342
345
|
|
|
@@ -355,7 +358,8 @@ Custom services integrate with the same pattern:
|
|
|
355
358
|
1. **Document all commands in SERVICE_README** - Required for discovery
|
|
356
359
|
2. **Use clear descriptions** - "Use when:" is crucial for semantic matching
|
|
357
360
|
3. **Include examples** - Helps AI agents understand usage
|
|
358
|
-
4. **
|
|
361
|
+
4. **Re-register after README changes** - `descix microservice register -r <file>` re-vectorizes
|
|
362
|
+
automatically; there is no separate vectorize step
|
|
359
363
|
|
|
360
364
|
### 7.3 Common Patterns
|
|
361
365
|
|
|
@@ -407,22 +411,11 @@ cd /path/to/workspace
|
|
|
407
411
|
|
|
408
412
|
### 8.3 "Command not found"
|
|
409
413
|
|
|
410
|
-
**Cause:** Service not registered or
|
|
414
|
+
**Cause:** Service not registered, or the README wasn't vectorized.
|
|
411
415
|
|
|
412
416
|
**Solution:**
|
|
413
|
-
1.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
## 9. File References
|
|
420
|
-
|
|
421
|
-
| Component | Path |
|
|
422
|
-
|-----------|------|
|
|
423
|
-
| MCP Tools | `DeSciX_Core/descix-cli/bin/mcp-server.js` |
|
|
424
|
-
| tell_me_how Implementation | `DeSciX_Cloud/microservice/services/commandHandlers/ragCommands.js` |
|
|
425
|
-
| execute_remote_command | `DeSciX_Cloud/microservice/services/apiFront.js` |
|
|
426
|
-
| SERVICE_README Template | `DeSciX_Core/descix-cli/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md` |
|
|
427
|
-
| Workspace Config | `DeSciX_Core/descix-cli/lib/workspace-config.js` |
|
|
428
|
-
| Service Registration | `DeSciX_Core/descix-cli/bin/descix.js` |
|
|
417
|
+
1. Re-run `descix microservice register -r SERVICE_README_<name>.md` — it re-publishes the
|
|
418
|
+
manifest and re-vectorizes the README (`descix microservice list` exists to enumerate
|
|
419
|
+
registered services, but it is an admin-only command, not something a developer runs)
|
|
420
|
+
2. Check `descix microservice health <name>` to confirm the service itself is reachable
|
|
421
|
+
3. Use `descix tell-me-how --scope discovery "..."` to find alternatives
|
|
@@ -160,13 +160,12 @@ descix site status -c egpt -a egpt-mydocs # site status/list need both when
|
|
|
160
160
|
|
|
161
161
|
---
|
|
162
162
|
|
|
163
|
-
## 7.
|
|
164
|
-
|
|
165
|
-
|
|
|
166
|
-
|
|
167
|
-
|
|
|
168
|
-
| Origin resolution
|
|
169
|
-
|
|
|
170
|
-
|
|
|
171
|
-
|
|
|
172
|
-
| Corpus sync | `DeSciX_Core/descix-cli/lib/commands/corpus.js` | manifests, sync-state |
|
|
163
|
+
## 7. Where each concept lives (conceptual, not a file map)
|
|
164
|
+
|
|
165
|
+
| Concept | Owner |
|
|
166
|
+
|---|---|
|
|
167
|
+
| Workspace loading, the v2.1 writer, app registration, context detection | `WorkspaceConfig`, internal to `@descix/cli` |
|
|
168
|
+
| Origin resolution precedence | flag → `DESCIX_API_URL` → `env.apiUrl` → default (`descix config` commands) |
|
|
169
|
+
| User-level settings (`~/.descixrc`) | `GlobalConfig`, internal to `@descix/cli` |
|
|
170
|
+
| `.descix/wallet.json` | written by `descix login` |
|
|
171
|
+
| Corpus manifests, sync-state | `descix kb corpus` commands, internal to `@descix/cli` |
|
package/bin/descix.js
CHANGED
|
@@ -34,6 +34,8 @@ import { createPromptSession } from '../lib/interactive.js';
|
|
|
34
34
|
import { registerAllRetiredKbSync, CANONICAL_KB_SYNC } from '../lib/commands/retired-kb-sync.js';
|
|
35
35
|
import { runStatus } from '../lib/commands/status.js';
|
|
36
36
|
import { refreshCommunityIdentity, printIdentityReceipt } from '../lib/commands/communityIdentity.js';
|
|
37
|
+
import { runAppInit } from '../lib/commands/appInit.js';
|
|
38
|
+
import { assertKnownCommandForHelp } from '../lib/help-guard.js';
|
|
37
39
|
import { runDoctor } from '../lib/commands/doctor.js';
|
|
38
40
|
import { runHealth } from '../lib/commands/health.js';
|
|
39
41
|
import * as kbCommands from '../lib/commands/kb.js';
|
|
@@ -1082,131 +1084,7 @@ appCommand
|
|
|
1082
1084
|
try {
|
|
1083
1085
|
const apiClient = new DeSciXApiClient();
|
|
1084
1086
|
await requireAuth(apiClient);
|
|
1085
|
-
|
|
1086
|
-
const kbId = options.kb || 'General';
|
|
1087
|
-
|
|
1088
|
-
// Refuse an unusable -p BEFORE anything is written — server-side included. The same
|
|
1089
|
-
// resolver the workspace loader applies on every read decides it, so a value accepted here
|
|
1090
|
-
// is one the next read accepts too. Refusing only at registration came AFTER a `-c` create,
|
|
1091
|
-
// leaving a platform app with no local registration (measured 2026-09-18, daita-docs).
|
|
1092
|
-
const workspaceConfig = await WorkspaceConfig.tryLoad();
|
|
1093
|
-
if (options.path) {
|
|
1094
|
-
resolveWorkspacePath(workspaceConfig?.workspaceRoot || process.cwd(), options.path, appId);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// Resolve community_id from the Products registry to learn whether the app exists yet.
|
|
1098
|
-
let communityId;
|
|
1099
|
-
try {
|
|
1100
|
-
const productCtx = await apiClient.invoke('get_product_context', { app_id: appId });
|
|
1101
|
-
communityId = (productCtx.message || productCtx).community_id;
|
|
1102
|
-
} catch (e) {
|
|
1103
|
-
// Not in Products — either we create it below (-c given) or we fail loud.
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
if (!communityId) {
|
|
1107
|
-
if (!options.community) {
|
|
1108
|
-
throw new Error(
|
|
1109
|
-
`App '${appId}' does not exist yet. Pass -c <community> to create it: ` +
|
|
1110
|
-
`descix app init -a ${appId} -c <community> [-s <short>]`
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
// CREATE leg — the canonical server path. It composes the unique app_id
|
|
1114
|
-
// ({community}-{short}) and is authoritative; we never compose it client-side.
|
|
1115
|
-
const response = await apiClient.invoke('create_app_for_community', {
|
|
1116
|
-
community_id: options.community,
|
|
1117
|
-
app_name: appId,
|
|
1118
|
-
short_name: options.short || undefined,
|
|
1119
|
-
create_skeleton: false,
|
|
1120
|
-
overwrite: options.overwrite,
|
|
1121
|
-
});
|
|
1122
|
-
const created = response.message || response;
|
|
1123
|
-
appId = created.app_id || appId;
|
|
1124
|
-
communityId = created.community_id || options.community;
|
|
1125
|
-
console.log(chalk.green(`\n✅ App created: ${appId}`));
|
|
1126
|
-
console.log(chalk.gray(` Products: Products/${appId}`));
|
|
1127
|
-
console.log(chalk.gray(` Firestore: Community/${communityId}/Apps/${appId}`));
|
|
1128
|
-
} else if (options.community && options.community !== communityId) {
|
|
1129
|
-
throw new Error(
|
|
1130
|
-
`App '${appId}' already exists in community '${communityId}', not '${options.community}'. ` +
|
|
1131
|
-
`Omit -c to initialize the existing app.`
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// 1. Workspace.json — register app if not already mapped
|
|
1136
|
-
const alreadyMapped = workspaceConfig?.getAppByAppId(appId);
|
|
1137
|
-
let appPath = alreadyMapped?.absolutePath;
|
|
1138
|
-
|
|
1139
|
-
if (alreadyMapped && options.path) {
|
|
1140
|
-
throw new Error(
|
|
1141
|
-
`App '${appId}' is already mapped to '${alreadyMapped.localPath}'. ` +
|
|
1142
|
-
`Use 'descix app set-localpath -a ${appId} -p <new-path>' to update.`
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
if (!alreadyMapped) {
|
|
1147
|
-
const localPath = options.path || '.';
|
|
1148
|
-
const wsRoot = workspaceConfig?.workspaceRoot || process.cwd();
|
|
1149
|
-
const cfg = workspaceConfig || new WorkspaceConfig({}, wsRoot);
|
|
1150
|
-
cfg.registerApp(communityId, appId, { localPath, kbId });
|
|
1151
|
-
await cfg.save(wsRoot);
|
|
1152
|
-
appPath = path.resolve(wsRoot, localPath);
|
|
1153
|
-
console.log(chalk.gray(` workspace.json updated: ${appId} → ${localPath}`));
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// 2. Create app folder structure (site, microservice, assets)
|
|
1157
|
-
if (appPath) {
|
|
1158
|
-
const siteDir = path.join(appPath, 'site');
|
|
1159
|
-
const msDir = path.join(appPath, 'microservice');
|
|
1160
|
-
const assetsDir = path.join(appPath, 'assets');
|
|
1161
|
-
await fs.mkdir(siteDir, { recursive: true });
|
|
1162
|
-
await fs.mkdir(msDir, { recursive: true });
|
|
1163
|
-
await fs.mkdir(assetsDir, { recursive: true });
|
|
1164
|
-
|
|
1165
|
-
// Create template asset files if they don't exist
|
|
1166
|
-
const siPath = path.join(assetsDir, 'system_instructions.md');
|
|
1167
|
-
const descPath = path.join(assetsDir, 'app_description.md');
|
|
1168
|
-
try {
|
|
1169
|
-
await fs.access(siPath);
|
|
1170
|
-
} catch {
|
|
1171
|
-
await fs.writeFile(siPath, `# System Instructions for ${appId}\n\nYou are an AI assistant for the ${appId} application.\n`);
|
|
1172
|
-
}
|
|
1173
|
-
try {
|
|
1174
|
-
await fs.access(descPath);
|
|
1175
|
-
} catch {
|
|
1176
|
-
await fs.writeFile(descPath, `# ${appId}\n\nApplication description goes here.\n`);
|
|
1177
|
-
}
|
|
1178
|
-
console.log(chalk.gray(` Created: site/, microservice/, assets/`));
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// 3. Create KnowledgeBase Firestore doc (Git Mode — no Drive required)
|
|
1182
|
-
const kbResponse = await apiClient.invoke('init_git_mode_kb', { app_id: appId, kb_name: kbId });
|
|
1183
|
-
const kbResult = kbResponse.message || kbResponse;
|
|
1184
|
-
|
|
1185
|
-
console.log(chalk.green(`\n✓ ${appId} initialized`));
|
|
1186
|
-
console.log(chalk.gray(` Community: ${communityId}`));
|
|
1187
|
-
|
|
1188
|
-
// A community's OWN app (app_id == community_id) mirrors its token symbol and icon from the
|
|
1189
|
-
// env-invariant descix-chain registry into this environment (CEO 2026-09-19). The app is
|
|
1190
|
-
// already initialized; a refusal here (not an admin of the community, or the community not in
|
|
1191
|
-
// the registry) is reported with the command that retries it, not treated as an init failure.
|
|
1192
|
-
if (appId === communityId) {
|
|
1193
|
-
try {
|
|
1194
|
-
printIdentityReceipt(communityId, await refreshCommunityIdentity(apiClient, communityId));
|
|
1195
|
-
} catch (err) {
|
|
1196
|
-
console.log(chalk.yellow(`\n ⚠ Community identity not mirrored: ${err.message}`));
|
|
1197
|
-
console.log(chalk.gray(` Retry with: descix community refresh-identity -c ${communityId}`));
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
console.log(chalk.gray(` KB: ${kbId} — ${kbResult.created ? 'created' : 'already exists'}\n`));
|
|
1201
|
-
console.log(chalk.gray(` (every app is guaranteed a default KB at creation; an empty one`));
|
|
1202
|
-
console.log(chalk.gray(` says so rather than answering from general knowledge)\n`));
|
|
1203
|
-
console.log(chalk.cyan('Next steps:'));
|
|
1204
|
-
// The manifest lives in THIS app's registered directory — never a guessed `apps/<id>/`,
|
|
1205
|
-
// which named a path that does not exist for any app registered elsewhere.
|
|
1206
|
-
const manifestPath = path.join(appPath, '.descix', 'manifests', `${kbId}.json`);
|
|
1207
|
-
console.log(chalk.gray(` Create a corpus manifest at ${path.relative(process.cwd(), manifestPath) || manifestPath}`));
|
|
1208
|
-
console.log(chalk.gray(` then run:`));
|
|
1209
|
-
console.log(chalk.white(` descix kb corpus sync -a ${appId}\n`));
|
|
1087
|
+
await runAppInit(apiClient, options);
|
|
1210
1088
|
} catch (error) {
|
|
1211
1089
|
console.error(chalk.red(error.message));
|
|
1212
1090
|
process.exit(1);
|
|
@@ -4581,10 +4459,10 @@ devCertsCommand
|
|
|
4581
4459
|
|
|
4582
4460
|
program
|
|
4583
4461
|
.command('quickstart')
|
|
4584
|
-
.description('One-command setup:
|
|
4462
|
+
.description('One-command setup: sign in → workspace → app (with -c and -a) → agent files → MCP config')
|
|
4585
4463
|
.option('-u, --url <url>', 'API URL override')
|
|
4586
|
-
.option('-c, --community <id>', 'Community
|
|
4587
|
-
.option('-a, --app <name>', '
|
|
4464
|
+
.option('-c, --community <id>', 'Community to create the app in')
|
|
4465
|
+
.option('-a, --app <name>', 'Name of the app to create (the platform issues its id; see app init)')
|
|
4588
4466
|
.option('-y, --yes', 'Skip the confirmation — with -c and -a, quickstart runs without a terminal')
|
|
4589
4467
|
.action(async (options) => {
|
|
4590
4468
|
// Every failure prints as a message and exits 1 — never a stack trace. A new developer's AI
|
|
@@ -4652,11 +4530,26 @@ program
|
|
|
4652
4530
|
// same one-owner rule that fixed the check above.
|
|
4653
4531
|
const targetRoot = existingRoot || workspaceRoot;
|
|
4654
4532
|
|
|
4655
|
-
// Step 3:
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4533
|
+
// Step 3: The app, and the agent instruction files that state its id. The id is the
|
|
4534
|
+
// platform's, so the files are written only once the workspace names one it issued: from the
|
|
4535
|
+
// workspace when an app is already registered, or by `app init` (the one owner, which writes
|
|
4536
|
+
// them itself) when -c and -a name the app to create.
|
|
4537
|
+
const { readIdentity, isIdentityNamed } = await import('../lib/workspace-identity.js');
|
|
4538
|
+
const wsRaw = JSON.parse(await fs.readFile(path.join(targetRoot, '.descix', 'workspace.json'), 'utf-8'));
|
|
4539
|
+
if (isIdentityNamed(readIdentity(wsRaw))) {
|
|
4540
|
+
console.log(chalk.cyan('\n📋 Generating Agent Instructions\n'));
|
|
4541
|
+
const written = await generateAgentFiles(targetRoot);
|
|
4542
|
+
for (const f of written) {
|
|
4543
|
+
console.log(chalk.green(` ✓ ${f}`));
|
|
4544
|
+
}
|
|
4545
|
+
} else if (options.community && options.app) {
|
|
4546
|
+
console.log(chalk.cyan('\n📋 Create and initialize the app\n'));
|
|
4547
|
+
const apiClient = new DeSciXApiClient();
|
|
4548
|
+
await requireAuth(apiClient);
|
|
4549
|
+
await runAppInit(apiClient, { app: options.app, community: options.community });
|
|
4550
|
+
} else {
|
|
4551
|
+
console.log(chalk.yellow('\n No app yet, so no agent instruction files (they state its id). Next:'));
|
|
4552
|
+
console.log(chalk.white(' descix app init -a <name> -c <community>'));
|
|
4660
4553
|
}
|
|
4661
4554
|
|
|
4662
4555
|
// Step 4: Generate .vscode/mcp.json (skipped if DeSciX extension handles MCP)
|
|
@@ -4758,6 +4651,10 @@ if (!adminMode && isHelpInvocation(program, rawArgv, { valueFlags: ['--env', '--
|
|
|
4758
4651
|
}
|
|
4759
4652
|
}
|
|
4760
4653
|
|
|
4654
|
+
// A command that does not exist must not appear to have help (see lib/help-guard.js). This runs
|
|
4655
|
+
// before the parse, because Commander resolves --help before it resolves the command.
|
|
4656
|
+
assertKnownCommandForHelp(program, process.argv.slice(2));
|
|
4657
|
+
|
|
4761
4658
|
// Parse arguments
|
|
4762
4659
|
await program.parseAsync(process.argv);
|
|
4763
4660
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `descix app init` — THE path to an app (CEO-D-2026-08-14): creates the app when it does not exist
|
|
3
|
+
* yet, then registers and scaffolds it locally. `descix quickstart` runs the same function, so the
|
|
4
|
+
* two cannot disagree about what an initialized app is.
|
|
5
|
+
*
|
|
6
|
+
* THE APP ID IS THE SERVER'S. With -c, the -a value is a NAME and create_app_for_community composes
|
|
7
|
+
* the id ({community}-{short}); this module registers whatever id the server returns and never
|
|
8
|
+
* composes one. `descix init` used to record the NAME as the app id, so the workspace and CLAUDE.md
|
|
9
|
+
* named `myapp` while the platform held `daita-myapp` (devx review 2026-09-19, D2). The agent
|
|
10
|
+
* instruction files are therefore written HERE, after the id exists, and nowhere earlier.
|
|
11
|
+
*/
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { WorkspaceConfig, resolveWorkspacePath } from '../workspace-config.js';
|
|
16
|
+
import { refreshCommunityIdentity, printIdentityReceipt } from './communityIdentity.js';
|
|
17
|
+
import { generateAgentFiles } from '../agent-files.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} apiClient an authenticated DeSciXApiClient
|
|
21
|
+
* @param {{app: string, community?: string, short?: string, overwrite?: boolean, kb?: string, path?: string}} options
|
|
22
|
+
* @returns {Promise<{appId: string, communityId: string, appPath: string, kbId: string}>}
|
|
23
|
+
*/
|
|
24
|
+
export async function runAppInit(apiClient, options) {
|
|
25
|
+
let appId = options.app;
|
|
26
|
+
const kbId = options.kb || 'General';
|
|
27
|
+
|
|
28
|
+
// Refuse an unusable -p BEFORE anything is written — server-side included. The same resolver
|
|
29
|
+
// the workspace loader applies on every read decides it, so a value accepted here is one the
|
|
30
|
+
// next read accepts too. Refusing only at registration came AFTER a `-c` create, leaving a
|
|
31
|
+
// platform app with no local registration (measured 2026-09-18, daita-docs).
|
|
32
|
+
const workspaceConfig = await WorkspaceConfig.tryLoad();
|
|
33
|
+
if (options.path) {
|
|
34
|
+
resolveWorkspacePath(workspaceConfig?.workspaceRoot || process.cwd(), options.path, appId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve community_id from the Products registry to learn whether the app exists yet.
|
|
38
|
+
let communityId;
|
|
39
|
+
try {
|
|
40
|
+
const productCtx = await apiClient.invoke('get_product_context', { app_id: appId });
|
|
41
|
+
communityId = (productCtx.message || productCtx).community_id;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Not in Products — either we create it below (-c given) or we fail loud.
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!communityId) {
|
|
47
|
+
if (!options.community) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`App '${appId}' does not exist yet. Pass -c <community> to create it: ` +
|
|
50
|
+
`descix app init -a ${appId} -c <community> [-s <short>]`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
// CREATE leg — the canonical server path. It composes the unique app_id
|
|
54
|
+
// ({community}-{short}) and is authoritative; we never compose it client-side.
|
|
55
|
+
const response = await apiClient.invoke('create_app_for_community', {
|
|
56
|
+
community_id: options.community,
|
|
57
|
+
app_name: appId,
|
|
58
|
+
short_name: options.short || undefined,
|
|
59
|
+
create_skeleton: false,
|
|
60
|
+
overwrite: options.overwrite,
|
|
61
|
+
});
|
|
62
|
+
const created = response.message || response;
|
|
63
|
+
appId = created.app_id || appId;
|
|
64
|
+
communityId = created.community_id || options.community;
|
|
65
|
+
console.log(chalk.green(`\n✅ App created: ${appId}`));
|
|
66
|
+
if (appId !== options.app) {
|
|
67
|
+
console.log(chalk.gray(` The platform composed the id from the community and the name you gave.`));
|
|
68
|
+
console.log(chalk.gray(` Use -a ${appId} in every later command.`));
|
|
69
|
+
}
|
|
70
|
+
} else if (options.community && options.community !== communityId) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`App '${appId}' already exists in community '${communityId}', not '${options.community}'. ` +
|
|
73
|
+
`Omit -c to initialize the existing app.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 1. Workspace.json — register app if not already mapped
|
|
78
|
+
const alreadyMapped = workspaceConfig?.getAppByAppId(appId);
|
|
79
|
+
let appPath = alreadyMapped?.absolutePath;
|
|
80
|
+
|
|
81
|
+
if (alreadyMapped && options.path) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`App '${appId}' is already mapped to '${alreadyMapped.localPath}'. ` +
|
|
84
|
+
`Use 'descix app set-localpath -a ${appId} -p <new-path>' to update.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const wsRoot = workspaceConfig?.workspaceRoot || process.cwd();
|
|
89
|
+
if (!alreadyMapped) {
|
|
90
|
+
const localPath = options.path || '.';
|
|
91
|
+
const cfg = workspaceConfig || new WorkspaceConfig({}, wsRoot);
|
|
92
|
+
cfg.registerApp(communityId, appId, { localPath, kbId });
|
|
93
|
+
await cfg.save(wsRoot);
|
|
94
|
+
appPath = path.resolve(wsRoot, localPath);
|
|
95
|
+
console.log(chalk.gray(` workspace.json updated: ${appId} → ${localPath}`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Create app folder structure (site, microservice, assets)
|
|
99
|
+
const assetsDir = path.join(appPath, 'assets');
|
|
100
|
+
await fs.mkdir(path.join(appPath, 'site'), { recursive: true });
|
|
101
|
+
await fs.mkdir(path.join(appPath, 'microservice'), { recursive: true });
|
|
102
|
+
await fs.mkdir(assetsDir, { recursive: true });
|
|
103
|
+
const siPath = path.join(assetsDir, 'system_instructions.md');
|
|
104
|
+
const descPath = path.join(assetsDir, 'app_description.md');
|
|
105
|
+
try {
|
|
106
|
+
await fs.access(siPath);
|
|
107
|
+
} catch {
|
|
108
|
+
await fs.writeFile(siPath, `# System Instructions for ${appId}\n\nYou are an AI assistant for the ${appId} application.\n`);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(descPath);
|
|
112
|
+
} catch {
|
|
113
|
+
await fs.writeFile(descPath, `# ${appId}\n\nApplication description goes here.\n`);
|
|
114
|
+
}
|
|
115
|
+
console.log(chalk.gray(` Created: site/, microservice/, assets/`));
|
|
116
|
+
|
|
117
|
+
// 3. Create KnowledgeBase Firestore doc (Git Mode — no Drive required)
|
|
118
|
+
const kbResponse = await apiClient.invoke('init_git_mode_kb', { app_id: appId, kb_name: kbId });
|
|
119
|
+
const kbResult = kbResponse.message || kbResponse;
|
|
120
|
+
|
|
121
|
+
console.log(chalk.green(`\n✓ ${appId} initialized`));
|
|
122
|
+
console.log(chalk.gray(` Community: ${communityId}`));
|
|
123
|
+
|
|
124
|
+
// A community's OWN app (app_id == community_id) mirrors its token symbol and icon from the
|
|
125
|
+
// env-invariant descix-chain registry into this environment (CEO 2026-09-19). The app is
|
|
126
|
+
// already initialized; a refusal here (not an admin of the community, or the community not in
|
|
127
|
+
// the registry) is reported with the command that retries it, not treated as an init failure.
|
|
128
|
+
if (appId === communityId) {
|
|
129
|
+
try {
|
|
130
|
+
printIdentityReceipt(communityId, await refreshCommunityIdentity(apiClient, communityId));
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.log(chalk.yellow(`\n ⚠ Community identity not mirrored: ${err.message}`));
|
|
133
|
+
console.log(chalk.gray(` Retry with: descix community refresh-identity -c ${communityId}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
console.log(chalk.gray(` KB: ${kbId} — ${kbResult.created ? 'created' : 'already exists'}\n`));
|
|
137
|
+
console.log(chalk.gray(` (every app is guaranteed a default KB at creation; an empty one`));
|
|
138
|
+
console.log(chalk.gray(` says so rather than answering from general knowledge)\n`));
|
|
139
|
+
|
|
140
|
+
// 4. Agent instruction files, now that the workspace names an id the platform issued. Files the
|
|
141
|
+
// developer has customised (no BOOTSTRAP marker) are left alone by generateAgentFiles.
|
|
142
|
+
const written = await generateAgentFiles(wsRoot);
|
|
143
|
+
for (const f of written) console.log(chalk.green(` ✓ ${f}`));
|
|
144
|
+
|
|
145
|
+
console.log(chalk.cyan('\nNext steps:'));
|
|
146
|
+
// The manifest lives in THIS app's registered directory — never a guessed `apps/<id>/`,
|
|
147
|
+
// which named a path that does not exist for any app registered elsewhere.
|
|
148
|
+
const manifestPath = path.join(appPath, '.descix', 'manifests', `${kbId}.json`);
|
|
149
|
+
console.log(chalk.gray(` Create a corpus manifest at ${path.relative(process.cwd(), manifestPath) || manifestPath}`));
|
|
150
|
+
console.log(chalk.gray(` then run:`));
|
|
151
|
+
console.log(chalk.white(` descix kb corpus sync -a ${appId}\n`));
|
|
152
|
+
|
|
153
|
+
return { appId, communityId, appPath, kbId };
|
|
154
|
+
}
|
package/lib/commands/init.js
CHANGED
|
@@ -11,7 +11,6 @@ import { fileURLToPath } from 'url';
|
|
|
11
11
|
import { dirname, join } from 'path';
|
|
12
12
|
// clone.js is imported dynamically inside the invite flow to avoid circular deps
|
|
13
13
|
import { WorkspaceConfig } from '../workspace-config.js';
|
|
14
|
-
import { generateAgentFiles } from '../agent-files.js';
|
|
15
14
|
// The canonical KB-sync surface is owned by retired-kb-sync.js. Consume the constant: a literal
|
|
16
15
|
// spelled here is a second derivation of the same fact, and the removed verb this replaced
|
|
17
16
|
// reached a developer who had done everything right.
|
|
@@ -151,11 +150,10 @@ export async function runInit(apiClient, options = {}) {
|
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
const appId = appName.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
155
153
|
console.log(chalk.cyan('\n─── Summary ───\n'));
|
|
156
154
|
console.log(chalk.white(` Project: ${projectPath}`));
|
|
157
155
|
console.log(chalk.white(` Community: ${communityId}`));
|
|
158
|
-
console.log(chalk.white(` App:
|
|
156
|
+
console.log(chalk.white(` App name: ${appName}`));
|
|
159
157
|
|
|
160
158
|
const proceed = options.yes ? true : await rl.askYesNo(chalk.white('\nProceed?'), true);
|
|
161
159
|
if (!proceed) {
|
|
@@ -170,7 +168,11 @@ export async function runInit(apiClient, options = {}) {
|
|
|
170
168
|
config.env.products = [];
|
|
171
169
|
delete config.env.platform;
|
|
172
170
|
}
|
|
173
|
-
|
|
171
|
+
// NO APP IS REGISTERED HERE. The app id is the platform's: `app init` creates the app and
|
|
172
|
+
// registers the id the server returns (with -c it is composed from the community and the
|
|
173
|
+
// name). Recording the NAME here as the id left the workspace and CLAUDE.md naming an app the
|
|
174
|
+
// platform does not have (devx review 2026-09-19, D2). The agent instruction files state that
|
|
175
|
+
// id, so `app init` writes them too, once it exists.
|
|
174
176
|
await config.save(projectPath);
|
|
175
177
|
// A NEW workspace records the environment this run used (`descix --env dev init …`), through
|
|
176
178
|
// the same owner `config init` uses — otherwise the workspace carried no environment and the
|
|
@@ -180,24 +182,16 @@ export async function runInit(apiClient, options = {}) {
|
|
|
180
182
|
await config.setEnvironment(options.env);
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
console.log(chalk.green('
|
|
185
|
+
console.log(chalk.green(`${existingConfig ? 'Updated' : 'Created'}:`));
|
|
184
186
|
console.log(chalk.green(' ✓ .descix/workspace.json'));
|
|
185
187
|
|
|
186
|
-
// Generate agent instruction files with correct context
|
|
187
|
-
try {
|
|
188
|
-
const agentFilesWritten = await generateAgentFiles(projectPath);
|
|
189
|
-
for (const f of agentFilesWritten) {
|
|
190
|
-
console.log(chalk.green(` ✓ ${f}`));
|
|
191
|
-
}
|
|
192
|
-
} catch {
|
|
193
|
-
// Agent file generation is best-effort
|
|
194
|
-
}
|
|
195
|
-
|
|
196
188
|
console.log(chalk.cyan('\n─── Next Steps ───\n'));
|
|
197
|
-
console.log(chalk.white(` 1. descix
|
|
198
|
-
console.log(chalk.
|
|
199
|
-
console.log(chalk.
|
|
200
|
-
console.log(chalk.
|
|
189
|
+
console.log(chalk.white(` 1. descix login # if you are not signed in`));
|
|
190
|
+
console.log(chalk.white(` 2. descix app init -a ${appName} -c ${communityId}`));
|
|
191
|
+
console.log(chalk.gray(` Creates the app and registers the id the platform issues for it (with -c the`));
|
|
192
|
+
console.log(chalk.gray(` id is ${communityId}-<name>), then writes CLAUDE.md and the other agent files.`));
|
|
193
|
+
console.log(chalk.gray(` 3. Create the corpus manifest app init names, then:`));
|
|
194
|
+
console.log(chalk.white(` ${CANONICAL_KB_SYNC} -a <the id app init printed>\n`));
|
|
201
195
|
console.log(chalk.green('✅ Workspace initialized.\n'));
|
|
202
196
|
rl.close();
|
|
203
197
|
return { created: ['.descix/workspace.json'], skipped: [], warnings: [] };
|
|
@@ -208,38 +202,4 @@ export async function runInit(apiClient, options = {}) {
|
|
|
208
202
|
}
|
|
209
203
|
}
|
|
210
204
|
|
|
211
|
-
|
|
212
|
-
* Non-interactive init: writes .descix/workspace.json only.
|
|
213
|
-
* @param {Object} options - { path?, communityId, appName? }
|
|
214
|
-
* @returns {Promise<Object>} { created, skipped, warnings }
|
|
215
|
-
*/
|
|
216
|
-
export async function initWorkspace(options) {
|
|
217
|
-
const projectPath = options.path ? path.resolve(options.path) : process.cwd();
|
|
218
|
-
const communityId = options.communityId;
|
|
219
|
-
const appName = options.appName || path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
220
|
-
const appId = appName.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
221
|
-
if (!communityId) {
|
|
222
|
-
throw new Error('communityId is required');
|
|
223
|
-
}
|
|
224
|
-
const config = new WorkspaceConfig({ version: '2.0', type: 'workspace', communities: {} }, projectPath);
|
|
225
|
-
config.registerApp(communityId, appId, { localPath: '.', kbId: 'General' });
|
|
226
|
-
await config.save(projectPath);
|
|
227
|
-
|
|
228
|
-
// Generate agent instruction files with workspace context
|
|
229
|
-
const created = ['.descix/workspace.json'];
|
|
230
|
-
try {
|
|
231
|
-
const agentFilesWritten = await generateAgentFiles(projectPath);
|
|
232
|
-
created.push(...agentFilesWritten);
|
|
233
|
-
} catch {
|
|
234
|
-
// Best-effort
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
created,
|
|
239
|
-
skipped: [],
|
|
240
|
-
warnings: [],
|
|
241
|
-
nextSteps: [`descix app init -a ${appId} -c ${communityId}`, `${CANONICAL_KB_SYNC} -a ${appId}`]
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export default { runInit, initWorkspace };
|
|
205
|
+
export default { runInit };
|