@descix/cli 1.0.15 → 1.0.17
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/bin/descix.js +146 -193
- package/bin/mcp-server.js +15 -4
- package/lib/agent-files.js +24 -4
- package/lib/auth-guard.js +1 -1
- package/lib/commands/appInit.js +154 -0
- package/lib/commands/communityIdentity.js +45 -0
- package/lib/commands/init.js +42 -71
- package/lib/interactive.js +22 -0
- package/lib/invocation.js +26 -0
- package/lib/verb-invokes.generated.json +10 -2
- package/lib/workspace-config.js +2 -1
- package/package.json +1 -1
- package/templates/agent-claude.md +18 -8
- package/templates/agent-cline.md +5 -5
- package/templates/agent-copilot.md +5 -5
- package/templates/agent-cursor.md +5 -5
package/bin/descix.js
CHANGED
|
@@ -33,6 +33,8 @@ import { runInit } from '../lib/commands/init.js';
|
|
|
33
33
|
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
|
+
import { refreshCommunityIdentity, printIdentityReceipt } from '../lib/commands/communityIdentity.js';
|
|
37
|
+
import { runAppInit } from '../lib/commands/appInit.js';
|
|
36
38
|
import { runDoctor } from '../lib/commands/doctor.js';
|
|
37
39
|
import { runHealth } from '../lib/commands/health.js';
|
|
38
40
|
import * as kbCommands from '../lib/commands/kb.js';
|
|
@@ -199,7 +201,8 @@ program
|
|
|
199
201
|
.option('-c, --community <id>', 'Community ID')
|
|
200
202
|
.option('-a, --app <name>', 'App name')
|
|
201
203
|
.option('-p, --path <path>', 'Project path (defaults to current directory)')
|
|
202
|
-
.option('-f, --force', '
|
|
204
|
+
.option('-f, --force', 'Restart the app registrations in an existing workspace (its environment settings are kept)')
|
|
205
|
+
.option('-y, --yes', 'Skip the confirmation — with -c and -a, init runs without a terminal')
|
|
203
206
|
.option('--from-invite <token>', 'Resolve an invite token to pre-fill app context')
|
|
204
207
|
.action(async (options) => {
|
|
205
208
|
try {
|
|
@@ -212,7 +215,7 @@ program
|
|
|
212
215
|
// Continue without API client
|
|
213
216
|
}
|
|
214
217
|
|
|
215
|
-
await runInit(apiClient, options);
|
|
218
|
+
await runInit(apiClient, { ...options, env: program.opts().env });
|
|
216
219
|
} catch (error) {
|
|
217
220
|
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
218
221
|
process.exit(1);
|
|
@@ -461,6 +464,22 @@ const communityCommand = program
|
|
|
461
464
|
.command('community')
|
|
462
465
|
.description('Community operations');
|
|
463
466
|
|
|
467
|
+
communityCommand
|
|
468
|
+
.command('refresh-identity')
|
|
469
|
+
.description('Mirror a community\'s token symbol and icon from the descix-chain registry into this environment (the icon only if it serves)')
|
|
470
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
471
|
+
.action(async (options) => {
|
|
472
|
+
try {
|
|
473
|
+
const apiClient = new DeSciXApiClient();
|
|
474
|
+
await requireAuth(apiClient);
|
|
475
|
+
printIdentityReceipt(options.community, await refreshCommunityIdentity(apiClient, options.community));
|
|
476
|
+
console.log();
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
464
483
|
communityCommand
|
|
465
484
|
.command('list')
|
|
466
485
|
.description('List all communities')
|
|
@@ -1064,107 +1083,7 @@ appCommand
|
|
|
1064
1083
|
try {
|
|
1065
1084
|
const apiClient = new DeSciXApiClient();
|
|
1066
1085
|
await requireAuth(apiClient);
|
|
1067
|
-
|
|
1068
|
-
const kbId = options.kb || 'General';
|
|
1069
|
-
|
|
1070
|
-
// Resolve community_id from the Products registry to learn whether the app exists yet.
|
|
1071
|
-
let communityId;
|
|
1072
|
-
try {
|
|
1073
|
-
const productCtx = await apiClient.invoke('get_product_context', { app_id: appId });
|
|
1074
|
-
communityId = (productCtx.message || productCtx).community_id;
|
|
1075
|
-
} catch (e) {
|
|
1076
|
-
// Not in Products — either we create it below (-c given) or we fail loud.
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (!communityId) {
|
|
1080
|
-
if (!options.community) {
|
|
1081
|
-
throw new Error(
|
|
1082
|
-
`App '${appId}' does not exist yet. Pass -c <community> to create it: ` +
|
|
1083
|
-
`descix app init -a ${appId} -c <community> [-s <short>]`
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
// CREATE leg — the canonical server path. It composes the unique app_id
|
|
1087
|
-
// ({community}-{short}) and is authoritative; we never compose it client-side.
|
|
1088
|
-
const response = await apiClient.invoke('create_app_for_community', {
|
|
1089
|
-
community_id: options.community,
|
|
1090
|
-
app_name: appId,
|
|
1091
|
-
short_name: options.short || undefined,
|
|
1092
|
-
create_skeleton: false,
|
|
1093
|
-
overwrite: options.overwrite,
|
|
1094
|
-
});
|
|
1095
|
-
const created = response.message || response;
|
|
1096
|
-
appId = created.app_id || appId;
|
|
1097
|
-
communityId = created.community_id || options.community;
|
|
1098
|
-
console.log(chalk.green(`\n✅ App created: ${appId}`));
|
|
1099
|
-
console.log(chalk.gray(` Products: Products/${appId}`));
|
|
1100
|
-
console.log(chalk.gray(` Firestore: Community/${communityId}/Apps/${appId}`));
|
|
1101
|
-
} else if (options.community && options.community !== communityId) {
|
|
1102
|
-
throw new Error(
|
|
1103
|
-
`App '${appId}' already exists in community '${communityId}', not '${options.community}'. ` +
|
|
1104
|
-
`Omit -c to initialize the existing app.`
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// 1. Workspace.json — register app if not already mapped
|
|
1109
|
-
const workspaceConfig = await WorkspaceConfig.tryLoad();
|
|
1110
|
-
const alreadyMapped = workspaceConfig?.getAppByAppId(appId);
|
|
1111
|
-
let appPath = alreadyMapped?.absolutePath;
|
|
1112
|
-
|
|
1113
|
-
if (alreadyMapped && options.path) {
|
|
1114
|
-
throw new Error(
|
|
1115
|
-
`App '${appId}' is already mapped to '${alreadyMapped.localPath}'. ` +
|
|
1116
|
-
`Use 'descix app set-localpath -a ${appId} -p <new-path>' to update.`
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
if (!alreadyMapped) {
|
|
1121
|
-
const localPath = options.path || '.';
|
|
1122
|
-
const wsRoot = workspaceConfig?.workspaceRoot || process.cwd();
|
|
1123
|
-
const cfg = workspaceConfig || new WorkspaceConfig({}, wsRoot);
|
|
1124
|
-
cfg.registerApp(communityId, appId, { localPath, kbId });
|
|
1125
|
-
await cfg.save(wsRoot);
|
|
1126
|
-
appPath = path.resolve(wsRoot, localPath);
|
|
1127
|
-
console.log(chalk.gray(` workspace.json updated: ${appId} → ${localPath}`));
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// 2. Create app folder structure (site, microservice, assets)
|
|
1131
|
-
if (appPath) {
|
|
1132
|
-
const siteDir = path.join(appPath, 'site');
|
|
1133
|
-
const msDir = path.join(appPath, 'microservice');
|
|
1134
|
-
const assetsDir = path.join(appPath, 'assets');
|
|
1135
|
-
await fs.mkdir(siteDir, { recursive: true });
|
|
1136
|
-
await fs.mkdir(msDir, { recursive: true });
|
|
1137
|
-
await fs.mkdir(assetsDir, { recursive: true });
|
|
1138
|
-
|
|
1139
|
-
// Create template asset files if they don't exist
|
|
1140
|
-
const siPath = path.join(assetsDir, 'system_instructions.md');
|
|
1141
|
-
const descPath = path.join(assetsDir, 'app_description.md');
|
|
1142
|
-
try {
|
|
1143
|
-
await fs.access(siPath);
|
|
1144
|
-
} catch {
|
|
1145
|
-
await fs.writeFile(siPath, `# System Instructions for ${appId}\n\nYou are an AI assistant for the ${appId} application.\n`);
|
|
1146
|
-
}
|
|
1147
|
-
try {
|
|
1148
|
-
await fs.access(descPath);
|
|
1149
|
-
} catch {
|
|
1150
|
-
await fs.writeFile(descPath, `# ${appId}\n\nApplication description goes here.\n`);
|
|
1151
|
-
}
|
|
1152
|
-
console.log(chalk.gray(` Created: site/, microservice/, assets/`));
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
// 3. Create KnowledgeBase Firestore doc (Git Mode — no Drive required)
|
|
1156
|
-
const kbResponse = await apiClient.invoke('init_git_mode_kb', { app_id: appId, kb_name: kbId });
|
|
1157
|
-
const kbResult = kbResponse.message || kbResponse;
|
|
1158
|
-
|
|
1159
|
-
console.log(chalk.green(`\n✓ ${appId} initialized`));
|
|
1160
|
-
console.log(chalk.gray(` Community: ${communityId}`));
|
|
1161
|
-
console.log(chalk.gray(` KB: ${kbId} — ${kbResult.created ? 'created' : 'already exists'}\n`));
|
|
1162
|
-
console.log(chalk.gray(` (every app is guaranteed a default KB at creation; an empty one`));
|
|
1163
|
-
console.log(chalk.gray(` says so rather than answering from general knowledge)\n`));
|
|
1164
|
-
console.log(chalk.cyan('Next steps:'));
|
|
1165
|
-
console.log(chalk.gray(` Create a corpus manifest at apps/${appId}/.descix/manifests/${kbId}.json`));
|
|
1166
|
-
console.log(chalk.gray(` then run:`));
|
|
1167
|
-
console.log(chalk.white(` descix kb corpus sync -a ${appId}\n`));
|
|
1086
|
+
await runAppInit(apiClient, options);
|
|
1168
1087
|
} catch (error) {
|
|
1169
1088
|
console.error(chalk.red(error.message));
|
|
1170
1089
|
process.exit(1);
|
|
@@ -2177,17 +2096,17 @@ const corpusCommand = kbCommand
|
|
|
2177
2096
|
|
|
2178
2097
|
corpusCommand
|
|
2179
2098
|
.command('sync')
|
|
2180
|
-
.description('Sync
|
|
2099
|
+
.description('Sync the files a corpus manifest names into the app\'s knowledge base')
|
|
2181
2100
|
.requiredOption('-a, --app <id>', 'App ID')
|
|
2182
2101
|
.option('-c, --community <community_id>', 'Community ID (optional; resolved from Products when omitted)')
|
|
2183
2102
|
.option('-k, --kb <name>', 'KB name (syncs specific manifest; default: all)')
|
|
2184
2103
|
.option('-v, --verbose', 'Show verbose output')
|
|
2185
|
-
.option('--ref <ref>', 'Override the git ref for ALL manifest sources (e.g., --ref
|
|
2186
|
-
.option('--rebuild', '
|
|
2187
|
-
.option('--dry-run', '
|
|
2188
|
-
.option('--show-walk', 'Print the resolved ref
|
|
2104
|
+
.option('--ref <ref>', 'Override the git ref for ALL manifest sources (e.g., --ref my-feature-branch). Precedence: --ref > manifest source.ref > "main".')
|
|
2105
|
+
.option('--rebuild', 'Delete EVERYTHING in the knowledge base, then re-sync the whole corpus. Not needed to remove deleted or changed files — an ordinary sync already removes any corpus file that is live in the knowledge base but no longer in the corpus (see --dry-run, which names them). Use --rebuild only to clear content an ordinary sync cannot see. Prompts before deleting unless --yes is supplied.')
|
|
2106
|
+
.option('--dry-run', 'Show what a sync would add and remove — naming each file it would remove — without changing anything. Exit 0 if nothing would change, 1 if something would.')
|
|
2107
|
+
.option('--show-walk', 'Print the resolved ref and the first 50 files the manifest selects, before syncing anything. Useful for checking --ref and the manifest sources.')
|
|
2189
2108
|
.option('--yes', 'Skip the interactive purge confirmation in --rebuild mode. Use in scripting/CI.')
|
|
2190
|
-
.option('--skip-retrieval-canary', 'Skip the post-sync
|
|
2109
|
+
.option('--skip-retrieval-canary', 'Skip the post-sync check that a synced passage is actually searchable (a large sync can take several minutes to become searchable). Skipping saves one credit-metered query and up to ~25 s; the sync then reports success on upload alone. Leave the check on for anything user-facing.')
|
|
2191
2110
|
.action(async (options) => {
|
|
2192
2111
|
try {
|
|
2193
2112
|
const apiClient = new DeSciXApiClient();
|
|
@@ -2317,7 +2236,7 @@ driveCommand
|
|
|
2317
2236
|
|
|
2318
2237
|
const siteCommand = program
|
|
2319
2238
|
.command('site')
|
|
2320
|
-
.description('
|
|
2239
|
+
.description('Build and publish your app\'s website');
|
|
2321
2240
|
|
|
2322
2241
|
// site init - Copy scaffold to app
|
|
2323
2242
|
siteCommand
|
|
@@ -2432,7 +2351,13 @@ siteCommand
|
|
|
2432
2351
|
// Import mime for content types
|
|
2433
2352
|
const mime = (await import('mime-types')).default;
|
|
2434
2353
|
const { GitUtils } = await import('@descix/sdk/integrations/git');
|
|
2435
|
-
|
|
2354
|
+
// Provenance is read from the directory the FILES come from — the app root when a site
|
|
2355
|
+
// manifest names the sources, else the -p directory — never the caller's cwd. Reading cwd
|
|
2356
|
+
// recorded the WRONG repository's commit on every deploy made from outside the app repo
|
|
2357
|
+
// (measured 2026-09-18: daita-docs, whose files live in DeSciX/daita, was stamped with the
|
|
2358
|
+
// Unkamon commit it happened to be run from).
|
|
2359
|
+
const provenanceDir = siteManifest ? appRoot : path.resolve(options.path);
|
|
2360
|
+
const gitUtils = new GitUtils(provenanceDir);
|
|
2436
2361
|
const gitStatus = await gitUtils.getStatus();
|
|
2437
2362
|
|
|
2438
2363
|
let fileList; // Array<{ path, hash, size, content_type }>
|
|
@@ -4271,7 +4196,7 @@ const mcpCommand = program
|
|
|
4271
4196
|
mcpCommand
|
|
4272
4197
|
.command('execute')
|
|
4273
4198
|
.description('Execute a registered MCP tool by name with JSON parameters')
|
|
4274
|
-
.requiredOption('--tool <name>', 'Tool/command name (e.g.,
|
|
4199
|
+
.requiredOption('--tool <name>', 'Tool/command name (e.g., find_communities, ask_question_to_app)')
|
|
4275
4200
|
.option('--params <json>', 'JSON parameters for the tool', '{}')
|
|
4276
4201
|
.option('-a, --app <app_id>', 'App context (sets app_id in params if not already present)')
|
|
4277
4202
|
.option('--json', 'Output raw JSON response')
|
|
@@ -4533,96 +4458,124 @@ devCertsCommand
|
|
|
4533
4458
|
|
|
4534
4459
|
program
|
|
4535
4460
|
.command('quickstart')
|
|
4536
|
-
.description('One-command setup:
|
|
4461
|
+
.description('One-command setup: sign in → workspace → app (with -c and -a) → agent files → MCP config')
|
|
4537
4462
|
.option('-u, --url <url>', 'API URL override')
|
|
4463
|
+
.option('-c, --community <id>', 'Community to create the app in')
|
|
4464
|
+
.option('-a, --app <name>', 'Name of the app to create (the platform issues its id; see app init)')
|
|
4465
|
+
.option('-y, --yes', 'Skip the confirmation — with -c and -a, quickstart runs without a terminal')
|
|
4538
4466
|
.action(async (options) => {
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
console.log(chalk.cyan('\n🚀 DeSciX Quickstart\n'));
|
|
4543
|
-
|
|
4544
|
-
const workspaceRoot = process.cwd();
|
|
4545
|
-
|
|
4546
|
-
// Step 1: Auth — login if no wallet.json
|
|
4547
|
-
const walletPath = WalletFileManager.getWalletPath(workspaceRoot);
|
|
4548
|
-
let needsLogin = true;
|
|
4467
|
+
// Every failure prints as a message and exits 1 — never a stack trace. A new developer's AI
|
|
4468
|
+
// assistant runs this non-interactively, and an uncaught refusal read as a crash (2026-09-19).
|
|
4549
4469
|
try {
|
|
4550
|
-
const
|
|
4551
|
-
|
|
4552
|
-
console.log(chalk.green(`✓ Already authenticated as ${wallet.userId}`));
|
|
4553
|
-
needsLogin = false;
|
|
4554
|
-
}
|
|
4555
|
-
} catch { /* no wallet */ }
|
|
4470
|
+
const { generateAgentFiles, generateMcpConfig, generateClaudeCodeMcpConfig } = await import('../lib/agent-files.js');
|
|
4471
|
+
const { WalletFileManager } = await import('../lib/wallet-file.js');
|
|
4556
4472
|
|
|
4557
|
-
|
|
4558
|
-
const loginOptions = {};
|
|
4559
|
-
if (options.url) loginOptions.url = options.url;
|
|
4560
|
-
await authCommands.loginDevice(loginOptions);
|
|
4561
|
-
}
|
|
4473
|
+
console.log(chalk.cyan('\n🚀 DeSciX Quickstart\n'));
|
|
4562
4474
|
|
|
4563
|
-
|
|
4564
|
-
//
|
|
4565
|
-
// WHERE THE WORKSPACE ROOT IS has ONE OWNER: WorkspaceConfig.findWorkspaceRoot, which walks UP
|
|
4566
|
-
// exactly as load() does. This step used to derive it a THIRD time with fs.access on the target
|
|
4567
|
-
// path only (the wizard guard was the second). Run from a SUBDIRECTORY of an existing
|
|
4568
|
-
// workspace, that check saw nothing, runInit created a NESTED workspace.json whose
|
|
4569
|
-
// workspaceRoot pointed at the subdirectory, and it SHADOWED the parent for every later
|
|
4570
|
-
// resolution — silently, with "Quickstart complete!" and exit 0.
|
|
4571
|
-
//
|
|
4572
|
-
// quickstart is an ONBOARDING flow: a user who asked to be set up and already IS set up has
|
|
4573
|
-
// SUCCEEDED, so this SKIPS and REPORTS and continues.
|
|
4574
|
-
const existingRoot = await WorkspaceConfig.findWorkspaceRoot(workspaceRoot);
|
|
4575
|
-
if (existingRoot) {
|
|
4576
|
-
console.log(chalk.green('✓ Workspace already initialized'));
|
|
4577
|
-
if (path.resolve(existingRoot) !== path.resolve(workspaceRoot)) {
|
|
4578
|
-
console.log(chalk.gray(` Workspace root: ${existingRoot}`));
|
|
4579
|
-
console.log(chalk.gray(` You are in a subdirectory of it. Not creating a second workspace`));
|
|
4580
|
-
console.log(chalk.gray(` here — a nested one would shadow the root for every command run`));
|
|
4581
|
-
console.log(chalk.gray(` from this directory.`));
|
|
4582
|
-
}
|
|
4583
|
-
} else {
|
|
4584
|
-
console.log(chalk.cyan('\n📋 Initialize Workspace\n'));
|
|
4585
|
-
// runInit's signature is (apiClient, options). Passing a single object put it in the
|
|
4586
|
-
// apiClient slot and left options defaulting to {}, so `path` was silently discarded and
|
|
4587
|
-
// runInit fell back to process.cwd(). It was MASKED here only because workspaceRoot IS
|
|
4588
|
-
// process.cwd() in this action — the argument was in the wrong slot regardless, and would
|
|
4589
|
-
// have written the workspace to the wrong directory the moment that stopped being true.
|
|
4590
|
-
// This action has no apiClient of its own; null is passed explicitly rather than implied.
|
|
4591
|
-
await runInit(null, { path: workspaceRoot });
|
|
4592
|
-
}
|
|
4475
|
+
const workspaceRoot = process.cwd();
|
|
4593
4476
|
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
for (const f of written) {
|
|
4605
|
-
console.log(chalk.green(` ✓ ${f}`));
|
|
4606
|
-
}
|
|
4477
|
+
// Step 1: Auth — login if no wallet.json
|
|
4478
|
+
const walletPath = WalletFileManager.getWalletPath(workspaceRoot);
|
|
4479
|
+
let needsLogin = true;
|
|
4480
|
+
try {
|
|
4481
|
+
const wallet = await WalletFileManager.loadWalletFile(walletPath);
|
|
4482
|
+
if (wallet && WalletFileManager.hasValidSession(wallet)) {
|
|
4483
|
+
console.log(chalk.green(`✓ Already authenticated as ${wallet.userId}`));
|
|
4484
|
+
needsLogin = false;
|
|
4485
|
+
}
|
|
4486
|
+
} catch { /* no wallet */ }
|
|
4607
4487
|
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
console.log(chalk.green(' ✓ MCP handled by DeSciX extension (mcp.json skipped)'));
|
|
4614
|
-
}
|
|
4488
|
+
if (needsLogin) {
|
|
4489
|
+
const loginOptions = {};
|
|
4490
|
+
if (options.url) loginOptions.url = options.url;
|
|
4491
|
+
await authCommands.loginDevice(loginOptions);
|
|
4492
|
+
}
|
|
4615
4493
|
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4494
|
+
// Step 2: Workspace init — create workspace.json only when there is none ANYWHERE UP THE TREE.
|
|
4495
|
+
//
|
|
4496
|
+
// WHERE THE WORKSPACE ROOT IS has ONE OWNER: WorkspaceConfig.findWorkspaceRoot, which walks UP
|
|
4497
|
+
// exactly as load() does. This step used to derive it a THIRD time with fs.access on the target
|
|
4498
|
+
// path only (the wizard guard was the second). Run from a SUBDIRECTORY of an existing
|
|
4499
|
+
// workspace, that check saw nothing, runInit created a NESTED workspace.json whose
|
|
4500
|
+
// workspaceRoot pointed at the subdirectory, and it SHADOWED the parent for every later
|
|
4501
|
+
// resolution — silently, with "Quickstart complete!" and exit 0.
|
|
4502
|
+
//
|
|
4503
|
+
// quickstart is an ONBOARDING flow: a user who asked to be set up and already IS set up has
|
|
4504
|
+
// SUCCEEDED, so this SKIPS and REPORTS and continues.
|
|
4505
|
+
const existingRoot = await WorkspaceConfig.findWorkspaceRoot(workspaceRoot);
|
|
4506
|
+
if (existingRoot) {
|
|
4507
|
+
console.log(chalk.green('✓ Workspace already initialized'));
|
|
4508
|
+
if (path.resolve(existingRoot) !== path.resolve(workspaceRoot)) {
|
|
4509
|
+
console.log(chalk.gray(` Workspace root: ${existingRoot}`));
|
|
4510
|
+
console.log(chalk.gray(` You are in a subdirectory of it. Not creating a second workspace`));
|
|
4511
|
+
console.log(chalk.gray(` here — a nested one would shadow the root for every command run`));
|
|
4512
|
+
console.log(chalk.gray(` from this directory.`));
|
|
4513
|
+
}
|
|
4514
|
+
} else {
|
|
4515
|
+
console.log(chalk.cyan('\n📋 Initialize Workspace\n'));
|
|
4516
|
+
// runInit's signature is (apiClient, options). Passing a single object put it in the
|
|
4517
|
+
// apiClient slot and left options defaulting to {}, so `path` was silently discarded and
|
|
4518
|
+
// runInit fell back to process.cwd(). It was MASKED here only because workspaceRoot IS
|
|
4519
|
+
// process.cwd() in this action — the argument was in the wrong slot regardless, and would
|
|
4520
|
+
// have written the workspace to the wrong directory the moment that stopped being true.
|
|
4521
|
+
// This action has no apiClient of its own; null is passed explicitly rather than implied.
|
|
4522
|
+
await runInit(null, { path: workspaceRoot, community: options.community, app: options.app, yes: options.yes, env: program.opts().env });
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
// EVERY REMAINING STEP TARGETS THE RESOLVED ROOT, not the directory the command was typed in.
|
|
4526
|
+
// They read the workspace to learn the app, community and origin they must state, so pointing
|
|
4527
|
+
// them at a subdirectory that deliberately has no workspace.json makes them hard-fail on the
|
|
4528
|
+
// absence this command just chose to preserve. Resolving the root once and ferrying it is the
|
|
4529
|
+
// same one-owner rule that fixed the check above.
|
|
4530
|
+
const targetRoot = existingRoot || workspaceRoot;
|
|
4531
|
+
|
|
4532
|
+
// Step 3: The app, and the agent instruction files that state its id. The id is the
|
|
4533
|
+
// platform's, so the files are written only once the workspace names one it issued: from the
|
|
4534
|
+
// workspace when an app is already registered, or by `app init` (the one owner, which writes
|
|
4535
|
+
// them itself) when -c and -a name the app to create.
|
|
4536
|
+
const { readIdentity, isIdentityNamed } = await import('../lib/workspace-identity.js');
|
|
4537
|
+
const wsRaw = JSON.parse(await fs.readFile(path.join(targetRoot, '.descix', 'workspace.json'), 'utf-8'));
|
|
4538
|
+
if (isIdentityNamed(readIdentity(wsRaw))) {
|
|
4539
|
+
console.log(chalk.cyan('\n📋 Generating Agent Instructions\n'));
|
|
4540
|
+
const written = await generateAgentFiles(targetRoot);
|
|
4541
|
+
for (const f of written) {
|
|
4542
|
+
console.log(chalk.green(` ✓ ${f}`));
|
|
4543
|
+
}
|
|
4544
|
+
} else if (options.community && options.app) {
|
|
4545
|
+
console.log(chalk.cyan('\n📋 Create and initialize the app\n'));
|
|
4546
|
+
const apiClient = new DeSciXApiClient();
|
|
4547
|
+
await requireAuth(apiClient);
|
|
4548
|
+
await runAppInit(apiClient, { app: options.app, community: options.community });
|
|
4549
|
+
} else {
|
|
4550
|
+
console.log(chalk.yellow('\n No app yet, so no agent instruction files (they state its id). Next:'));
|
|
4551
|
+
console.log(chalk.white(' descix app init -a <name> -c <community>'));
|
|
4552
|
+
}
|
|
4620
4553
|
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4554
|
+
// Step 4: Generate .vscode/mcp.json (skipped if DeSciX extension handles MCP)
|
|
4555
|
+
const mcpWritten = await generateMcpConfig(targetRoot);
|
|
4556
|
+
if (mcpWritten) {
|
|
4557
|
+
console.log(chalk.green(' ✓ .vscode/mcp.json'));
|
|
4558
|
+
} else {
|
|
4559
|
+
console.log(chalk.green(' ✓ MCP handled by DeSciX extension (mcp.json skipped)'));
|
|
4560
|
+
}
|
|
4561
|
+
// Claude Code reads project MCP servers from .mcp.json — written regardless of the VS Code case.
|
|
4562
|
+
await generateClaudeCodeMcpConfig(targetRoot);
|
|
4563
|
+
console.log(chalk.green(' ✓ .mcp.json (Claude Code)'));
|
|
4564
|
+
|
|
4565
|
+
// Step 5: Copy SDK assets
|
|
4566
|
+
const { pullSdkAssets } = await import('../lib/sdk-assets.js');
|
|
4567
|
+
await pullSdkAssets(targetRoot);
|
|
4568
|
+
console.log(chalk.green(' ✓ .descix/sdk-assets/'));
|
|
4569
|
+
|
|
4570
|
+
// Done
|
|
4571
|
+
console.log(chalk.green('\n✅ Quickstart complete!\n'));
|
|
4572
|
+
console.log(chalk.white('Open your editor — the AI knows about DeSciX.\n'));
|
|
4573
|
+
console.log(chalk.gray(' Copilot / Cline / Claude Code will have DeSciX MCP tools'));
|
|
4574
|
+
console.log(chalk.gray(' Ask: "What DeSciX tools do I have?"\n'));
|
|
4575
|
+
} catch (error) {
|
|
4576
|
+
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
4577
|
+
process.exit(1);
|
|
4578
|
+
}
|
|
4626
4579
|
});
|
|
4627
4580
|
|
|
4628
4581
|
// ============ MCP Server Command (for npx usage) ============
|
package/bin/mcp-server.js
CHANGED
|
@@ -31,7 +31,7 @@ import * as path from 'path';
|
|
|
31
31
|
// This stdio server imports them and CONCATENATES its CLI-LOCAL diagnostics (descix_doctor,
|
|
32
32
|
// platform_health) which are NOT /apifront commands and are intentionally stdio-only.
|
|
33
33
|
// The previously hand-duplicated curated literal in this file is GONE.
|
|
34
|
-
import { toMcpToolList, NATIVE_MCP_TOOLS as SHARED_NATIVE_TOOLS, MCP_HANDSHAKE_INSTRUCTIONS, validateToolParams, toolAcceptsParam, VALIDATION_PHASE } from '@descix/platform-api/mcp-tools';
|
|
34
|
+
import { toMcpToolList, NATIVE_MCP_TOOLS as SHARED_NATIVE_TOOLS, MCP_HANDSHAKE_INSTRUCTIONS, validateToolParams, toolAcceptsParam, VALIDATION_PHASE, isDiscoveryCoreTool } from '@descix/platform-api/mcp-tools';
|
|
35
35
|
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
37
|
// Tool definitions for the stdio MCP transport.
|
|
@@ -77,9 +77,20 @@ const CLI_LOCAL_TOOLS = [
|
|
|
77
77
|
},
|
|
78
78
|
];
|
|
79
79
|
|
|
80
|
-
//
|
|
80
|
+
// Every tool this server can DESCRIBE: CLI-local diagnostics + the shared curated core. It seeds
|
|
81
|
+
// the schema-aware tools/call path below, and is NEVER advertised as a whole.
|
|
81
82
|
const TOOLS = [...CLI_LOCAL_TOOLS, ...toMcpToolList(SHARED_NATIVE_TOOLS)];
|
|
82
83
|
|
|
84
|
+
// What tools/list ADVERTISES when the backend cannot supply the caller's permission-filtered
|
|
85
|
+
// list — which is every UNAUTHENTICATED session, i.e. an outside developer's first contact.
|
|
86
|
+
// It is the ratified DISCOVERY-CORE floor (platform-api isDiscoveryCoreTool, the same floor the
|
|
87
|
+
// server serves a caller with no grant), never the whole static catalogue: advertising all of
|
|
88
|
+
// TOOLS showed 37 tools, 18 of them the internal coordination fabric's (measured 2026-09-19).
|
|
89
|
+
const FALLBACK_ADVERTISED_TOOLS = [
|
|
90
|
+
...CLI_LOCAL_TOOLS,
|
|
91
|
+
...toMcpToolList(SHARED_NATIVE_TOOLS.filter((t) => isDiscoveryCoreTool(t.name))),
|
|
92
|
+
];
|
|
93
|
+
|
|
83
94
|
// Live tool surface as last returned by tools/list (permission-filtered by the backend).
|
|
84
95
|
// Seeded with the static curated SSOT so the very first tools/call is schema-aware even if
|
|
85
96
|
// no tools/list preceded it. Consumed by tools/call for schema-aware context injection and
|
|
@@ -410,8 +421,8 @@ try {
|
|
|
410
421
|
console.error(`[MCP] tools/list — ${backendTools.length} permission-filtered backend tools + ${CLI_LOCAL_TOOLS.length} CLI-local = ${tools.length}`);
|
|
411
422
|
return { tools };
|
|
412
423
|
} catch (err) {
|
|
413
|
-
console.error(`[MCP] tools/list — backend unreachable (${err.message});
|
|
414
|
-
return { tools:
|
|
424
|
+
console.error(`[MCP] tools/list — backend unreachable (${err.message}); advertising the discovery-core floor (${FALLBACK_ADVERTISED_TOOLS.length} tools)`);
|
|
425
|
+
return { tools: FALLBACK_ADVERTISED_TOOLS };
|
|
415
426
|
}
|
|
416
427
|
});
|
|
417
428
|
|
package/lib/agent-files.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from 'fs/promises';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { readIdentity, isIdentityNamed } from './workspace-identity.js';
|
|
14
|
+
import { mcpServerEntry } from './invocation.js';
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = path.dirname(__filename);
|
|
@@ -163,12 +164,31 @@ export async function generateMcpConfig(workspaceRoot) {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
// Add/update DeSciX MCP server
|
|
166
|
-
existing.servers['descix'] =
|
|
167
|
-
command: 'descix',
|
|
168
|
-
args: ['mcp-serve'],
|
|
169
|
-
};
|
|
167
|
+
existing.servers['descix'] = mcpServerEntry();
|
|
170
168
|
|
|
171
169
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
172
170
|
await fs.writeFile(outputPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
173
171
|
return true;
|
|
174
172
|
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Claude Code's PROJECT MCP config: `.mcp.json` at the project root, `{ mcpServers: { … } }`.
|
|
176
|
+
* Written independently of the VS Code file above (whose DeSciX-extension skip does not apply to
|
|
177
|
+
* Claude Code). Existing servers are preserved; only the `descix` entry is set.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} workspaceRoot
|
|
180
|
+
* @returns {Promise<boolean>} true when written
|
|
181
|
+
*/
|
|
182
|
+
export async function generateClaudeCodeMcpConfig(workspaceRoot) {
|
|
183
|
+
const outputPath = path.join(workspaceRoot, '.mcp.json');
|
|
184
|
+
let existing = { mcpServers: {} };
|
|
185
|
+
try {
|
|
186
|
+
existing = JSON.parse(await fs.readFile(outputPath, 'utf-8'));
|
|
187
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
188
|
+
} catch {
|
|
189
|
+
// No existing file — start fresh
|
|
190
|
+
}
|
|
191
|
+
existing.mcpServers.descix = mcpServerEntry();
|
|
192
|
+
await fs.writeFile(outputPath, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
|
|
193
|
+
return true;
|
|
194
|
+
}
|
package/lib/auth-guard.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function requireAuth(apiClient) {
|
|
|
29
29
|
|
|
30
30
|
if (!apiClient.hasCredentials()) {
|
|
31
31
|
console.error(chalk.red('\n Authentication required.\n'));
|
|
32
|
-
console.error(chalk.white('
|
|
32
|
+
console.error(chalk.white(' Sign in to use this. On first use, signing in also creates your DeSciX account.\n'));
|
|
33
33
|
console.error(chalk.cyan(' Run: descix login\n'));
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror a community's IDENTITY — token symbol and icon — from the env-invariant descix-chain
|
|
3
|
+
* registry into the current environment's records (CEO 2026-09-19).
|
|
4
|
+
*
|
|
5
|
+
* ONE owner for both callers: `descix app init` (run for a community's own app) and
|
|
6
|
+
* `descix community refresh-identity`. The server reads the registry and verifies the icon
|
|
7
|
+
* actually serves (update_community_metadata with from_registry); this module only asks and
|
|
8
|
+
* reports, so the receipt shows what the SERVER read and wrote, never what we hoped.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} apiClient
|
|
14
|
+
* @param {string} communityId
|
|
15
|
+
* @returns {Promise<Object>} the server's `registry` block
|
|
16
|
+
* @throws {Error} when the server returns no registry block — an unreported mirror is not a mirror
|
|
17
|
+
*/
|
|
18
|
+
export async function refreshCommunityIdentity(apiClient, communityId) {
|
|
19
|
+
const response = await apiClient.invoke('update_community_metadata', {
|
|
20
|
+
community_id: communityId,
|
|
21
|
+
from_registry: true,
|
|
22
|
+
});
|
|
23
|
+
const result = response.message || response;
|
|
24
|
+
if (!result || !result.registry) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`update_community_metadata returned no registry block for '${communityId}' — the server did ` +
|
|
27
|
+
`not report what it mirrored, so nothing can be claimed about it.`);
|
|
28
|
+
}
|
|
29
|
+
return result.registry;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Print what was read from the registry and what changed in this environment. */
|
|
33
|
+
export function printIdentityReceipt(communityId, registry) {
|
|
34
|
+
const { token_symbol, icon_url, icon_status, before, community_app_updated } = registry;
|
|
35
|
+
console.log(chalk.cyan(`\n Identity for ${communityId} (from the descix-chain registry):`));
|
|
36
|
+
const sym = before?.token_symbol === token_symbol ? 'unchanged' : `was ${before?.token_symbol ?? 'unset'}`;
|
|
37
|
+
console.log(chalk.gray(` Token symbol: ${token_symbol} (${sym})`));
|
|
38
|
+
if (icon_url) {
|
|
39
|
+
const ic = before?.icon_url === icon_url ? 'unchanged' : 'updated';
|
|
40
|
+
console.log(chalk.gray(` Icon: ${icon_url} (${ic}, ${icon_status})`));
|
|
41
|
+
} else {
|
|
42
|
+
console.log(chalk.yellow(` Icon: not written — the registry icon is ${icon_status}. The store shows a monogram.`));
|
|
43
|
+
}
|
|
44
|
+
console.log(chalk.gray(` Community app record: ${community_app_updated ? 'updated' : 'not found in this environment'}`));
|
|
45
|
+
}
|
package/lib/commands/init.js
CHANGED
|
@@ -11,13 +11,12 @@ 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.
|
|
18
17
|
import { CANONICAL_KB_SYNC } from './retired-kb-sync.js';
|
|
19
18
|
// "May I prompt?" has ONE OWNER. init does not derive it, and holds no TTY check of its own.
|
|
20
|
-
import {
|
|
19
|
+
import { createLazyPromptSession } from '../interactive.js';
|
|
21
20
|
|
|
22
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
22
|
const __dirname = dirname(__filename);
|
|
@@ -28,13 +27,15 @@ const __dirname = dirname(__filename);
|
|
|
28
27
|
* @param {Object} options - Command options
|
|
29
28
|
*/
|
|
30
29
|
export async function runInit(apiClient, options = {}) {
|
|
31
|
-
// The session is
|
|
32
|
-
//
|
|
33
|
-
// pipe
|
|
34
|
-
|
|
30
|
+
// The session is built only when a question is actually asked, and is still gated then: under
|
|
31
|
+
// a non-TTY an unanswered question throws NonInteractiveError (bin/descix.js prints it and
|
|
32
|
+
// exits 1) — it can never hang on an open pipe, nor exit 0 having written nothing. With every
|
|
33
|
+
// answer given as a flag, nothing is asked, so an AI assistant or a script can run init.
|
|
34
|
+
const rl = createLazyPromptSession({
|
|
35
35
|
what: 'descix init',
|
|
36
36
|
nonInteractiveForm: [
|
|
37
|
-
'descix init
|
|
37
|
+
'descix init -c <community> -a <app> --yes',
|
|
38
|
+
' (add --force to replace an existing .descix/workspace.json)',
|
|
38
39
|
'',
|
|
39
40
|
'Nothing has been changed. An existing .descix/workspace.json is left as it is.'
|
|
40
41
|
]
|
|
@@ -105,14 +106,12 @@ export async function runInit(apiClient, options = {}) {
|
|
|
105
106
|
// No existing config
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
}
|
|
109
|
+
// An existing workspace is EXTENDED, never replaced. It may carry the environment the developer
|
|
110
|
+
// chose with `descix config init --env dev` — env.apiUrl / env.environment, owned by the config
|
|
111
|
+
// verbs — and init used to write a brand-new file over it, silently moving a DEV workspace to
|
|
112
|
+
// PROD (measured 2026-09-19: DEV before, `config show` said prod after). --force now restarts
|
|
113
|
+
// only the APP registrations; the environment is kept either way.
|
|
114
|
+
const existingConfig = hasExisting ? await WorkspaceConfig.load(projectPath) : null;
|
|
116
115
|
|
|
117
116
|
let communityId = options.community;
|
|
118
117
|
if (!communityId) {
|
|
@@ -151,42 +150,48 @@ 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
|
-
const proceed = await rl.askYesNo(chalk.white('\nProceed?'), true);
|
|
158
|
+
const proceed = options.yes ? true : await rl.askYesNo(chalk.white('\nProceed?'), true);
|
|
161
159
|
if (!proceed) {
|
|
162
160
|
console.log(chalk.gray('\nCancelled.\n'));
|
|
163
161
|
rl.close();
|
|
164
162
|
return {};
|
|
165
163
|
}
|
|
166
164
|
|
|
167
|
-
console.log(chalk.gray('
|
|
168
|
-
const config = new WorkspaceConfig({ version: '2.
|
|
169
|
-
|
|
165
|
+
console.log(chalk.gray(`\n${existingConfig ? 'Updating' : 'Writing'} .descix/workspace.json...\n`));
|
|
166
|
+
const config = existingConfig || new WorkspaceConfig({ version: '2.1', type: 'workspace', env: {} }, projectPath);
|
|
167
|
+
if (existingConfig && options.force && config.env) {
|
|
168
|
+
config.env.products = [];
|
|
169
|
+
delete config.env.platform;
|
|
170
|
+
}
|
|
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.
|
|
170
176
|
await config.save(projectPath);
|
|
177
|
+
// A NEW workspace records the environment this run used (`descix --env dev init …`), through
|
|
178
|
+
// the same owner `config init` uses — otherwise the workspace carried no environment and the
|
|
179
|
+
// very next command fell back to PROD, although the developer had just chosen DEV. An EXISTING
|
|
180
|
+
// workspace keeps its own environment: that choice belongs to the config verbs.
|
|
181
|
+
if (!existingConfig && options.env) {
|
|
182
|
+
await config.setEnvironment(options.env);
|
|
183
|
+
}
|
|
171
184
|
|
|
172
|
-
console.log(chalk.green('
|
|
185
|
+
console.log(chalk.green(`${existingConfig ? 'Updated' : 'Created'}:`));
|
|
173
186
|
console.log(chalk.green(' ✓ .descix/workspace.json'));
|
|
174
187
|
|
|
175
|
-
// Generate agent instruction files with correct context
|
|
176
|
-
try {
|
|
177
|
-
const agentFilesWritten = await generateAgentFiles(projectPath);
|
|
178
|
-
for (const f of agentFilesWritten) {
|
|
179
|
-
console.log(chalk.green(` ✓ ${f}`));
|
|
180
|
-
}
|
|
181
|
-
} catch {
|
|
182
|
-
// Agent file generation is best-effort
|
|
183
|
-
}
|
|
184
|
-
|
|
185
188
|
console.log(chalk.cyan('\n─── Next Steps ───\n'));
|
|
186
|
-
console.log(chalk.white(` 1. descix
|
|
187
|
-
console.log(chalk.
|
|
188
|
-
console.log(chalk.
|
|
189
|
-
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`));
|
|
190
195
|
console.log(chalk.green('✅ Workspace initialized.\n'));
|
|
191
196
|
rl.close();
|
|
192
197
|
return { created: ['.descix/workspace.json'], skipped: [], warnings: [] };
|
|
@@ -197,38 +202,4 @@ export async function runInit(apiClient, options = {}) {
|
|
|
197
202
|
}
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
|
|
201
|
-
* Non-interactive init: writes .descix/workspace.json only.
|
|
202
|
-
* @param {Object} options - { path?, communityId, appName? }
|
|
203
|
-
* @returns {Promise<Object>} { created, skipped, warnings }
|
|
204
|
-
*/
|
|
205
|
-
export async function initWorkspace(options) {
|
|
206
|
-
const projectPath = options.path ? path.resolve(options.path) : process.cwd();
|
|
207
|
-
const communityId = options.communityId;
|
|
208
|
-
const appName = options.appName || path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
209
|
-
const appId = appName.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
210
|
-
if (!communityId) {
|
|
211
|
-
throw new Error('communityId is required');
|
|
212
|
-
}
|
|
213
|
-
const config = new WorkspaceConfig({ version: '2.0', type: 'workspace', communities: {} }, projectPath);
|
|
214
|
-
config.registerApp(communityId, appId, { localPath: '.', kbId: 'General' });
|
|
215
|
-
await config.save(projectPath);
|
|
216
|
-
|
|
217
|
-
// Generate agent instruction files with workspace context
|
|
218
|
-
const created = ['.descix/workspace.json'];
|
|
219
|
-
try {
|
|
220
|
-
const agentFilesWritten = await generateAgentFiles(projectPath);
|
|
221
|
-
created.push(...agentFilesWritten);
|
|
222
|
-
} catch {
|
|
223
|
-
// Best-effort
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
created,
|
|
228
|
-
skipped: [],
|
|
229
|
-
warnings: [],
|
|
230
|
-
nextSteps: [`descix app init -a ${appId} -c ${communityId}`, `${CANONICAL_KB_SYNC} -a ${appId}`]
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export default { runInit, initWorkspace };
|
|
205
|
+
export default { runInit };
|
package/lib/interactive.js
CHANGED
|
@@ -155,4 +155,26 @@ export function createPromptSession({ what, nonInteractiveForm, terminal, destru
|
|
|
155
155
|
return { ask, askYesNo, askRaw, close: () => rl.close() };
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* A prompt session that is built only when a question is actually ASKED.
|
|
160
|
+
*
|
|
161
|
+
* `createPromptSession` gates at construction, which is right for a command that always
|
|
162
|
+
* prompts. A command whose every answer can arrive as a FLAG must not refuse a non-terminal
|
|
163
|
+
* before it knows it needs one: that made `descix init -c <c> -a <a>` impossible to run from an
|
|
164
|
+
* AI assistant or a script, although nothing was left to ask (measured 2026-09-19). The gate is
|
|
165
|
+
* unchanged, and still the one owner — this only defers WHEN it is consulted.
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} o - exactly createPromptSession's options
|
|
168
|
+
* @returns {{ask: Function, askYesNo: Function, close: Function}}
|
|
169
|
+
*/
|
|
170
|
+
export function createLazyPromptSession(o = {}) {
|
|
171
|
+
let session = null;
|
|
172
|
+
const open = () => (session ||= createPromptSession(o));
|
|
173
|
+
return {
|
|
174
|
+
ask: (...args) => open().ask(...args),
|
|
175
|
+
askYesNo: (...args) => open().askYesNo(...args),
|
|
176
|
+
close: () => { if (session) session.close(); },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
158
180
|
export default { stdinIsInteractive, requireInteractive, createPromptSession, NonInteractiveError };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW A DEVELOPER (OR THEIR AI ASSISTANT) RUNS THIS CLI — the one owner.
|
|
3
|
+
*
|
|
4
|
+
* The npm package is `@descix/cli`; the binary it installs is `descix`. There is no npm package
|
|
5
|
+
* named `descix`, so `npx descix …` is a 404 — and it was typed, independently, into the
|
|
6
|
+
* "workspace not configured" remedy, the CLAUDE.md template and the docs (devx review 2026-09-19).
|
|
7
|
+
* The editor MCP config meanwhile used a bare `descix`, which exists only after a GLOBAL install,
|
|
8
|
+
* so an npx user's editor could not start the server.
|
|
9
|
+
*
|
|
10
|
+
* `npx -y -p @descix/cli descix …` works for both: with a global install npx finds the binary, and
|
|
11
|
+
* without one it fetches the package. `-y` stops npx from prompting, which an editor launching an
|
|
12
|
+
* MCP server (or an agent running a command) cannot answer.
|
|
13
|
+
*/
|
|
14
|
+
export const CLI_PACKAGE = '@descix/cli';
|
|
15
|
+
export const CLI_BIN = 'descix';
|
|
16
|
+
const NPX_PREFIX = ['-y', '-p', CLI_PACKAGE, CLI_BIN];
|
|
17
|
+
|
|
18
|
+
/** `npx -y -p @descix/cli descix <args…>` as one string, for a message a person copies. */
|
|
19
|
+
export function npxCommand(...args) {
|
|
20
|
+
return ['npx', ...NPX_PREFIX, ...args].join(' ');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The MCP server entry every editor config uses: VS Code `.vscode/mcp.json`, Claude Code `.mcp.json`. */
|
|
24
|
+
export function mcpServerEntry() {
|
|
25
|
+
return { command: 'npx', args: [...NPX_PREFIX, 'mcp-serve'] };
|
|
26
|
+
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"app init": [
|
|
12
12
|
"create_app_for_community",
|
|
13
13
|
"get_product_context",
|
|
14
|
-
"init_git_mode_kb"
|
|
14
|
+
"init_git_mode_kb",
|
|
15
|
+
"update_community_metadata"
|
|
15
16
|
],
|
|
16
17
|
"app invite": [
|
|
17
18
|
"create_app_invite"
|
|
@@ -90,6 +91,9 @@
|
|
|
90
91
|
"community list": [
|
|
91
92
|
"find_communities"
|
|
92
93
|
],
|
|
94
|
+
"community refresh-identity": [
|
|
95
|
+
"update_community_metadata"
|
|
96
|
+
],
|
|
93
97
|
"community rename": [
|
|
94
98
|
"rename_community"
|
|
95
99
|
],
|
|
@@ -186,11 +190,15 @@
|
|
|
186
190
|
"fetch_my_purchases"
|
|
187
191
|
],
|
|
188
192
|
"quickstart": [
|
|
193
|
+
"create_app_for_community",
|
|
189
194
|
"device_check_status",
|
|
190
195
|
"device_request_login",
|
|
191
196
|
"find_communities",
|
|
197
|
+
"get_product_context",
|
|
198
|
+
"init_git_mode_kb",
|
|
192
199
|
"resolve_invite",
|
|
193
|
-
"sync_auto_purchases"
|
|
200
|
+
"sync_auto_purchases",
|
|
201
|
+
"update_community_metadata"
|
|
194
202
|
],
|
|
195
203
|
"reconnect": [
|
|
196
204
|
"reconnect_by_wallet"
|
package/lib/workspace-config.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolveOrigin } from './origin.js';
|
|
|
6
6
|
// a stuck developer will copy verbatim, so it must resolve to the live verb rather than a literal
|
|
7
7
|
// that can go stale where nobody is watching.
|
|
8
8
|
import { CANONICAL_KB_SYNC } from './commands/retired-kb-sync.js';
|
|
9
|
+
import { npxCommand } from './invocation.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* THE ONE OWNER of "this app is not mapped in workspace.json, here is how to fix it".
|
|
@@ -61,7 +62,7 @@ export function resolveWorkspacePath(workspaceRoot, localPath, label = 'app') {
|
|
|
61
62
|
*/
|
|
62
63
|
const NOT_CONFIGURED_MESSAGE =
|
|
63
64
|
'Workspace not configured.\n' +
|
|
64
|
-
|
|
65
|
+
`Run "${npxCommand('init')}" first to initialize your workspace.`;
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* THE ONE OWNER of "how do I set the API origin". Named by every refusal that turns a developer
|
package/package.json
CHANGED
|
@@ -84,15 +84,18 @@ Use `find_communities` and `list_apps_for_community` to show options, then ask:
|
|
|
84
84
|
> "Which community and app should this project target?"
|
|
85
85
|
|
|
86
86
|
### Checkpoint 3: Environment
|
|
87
|
-
> "Should this
|
|
87
|
+
> "Should this app be built against DEV (https://dev.descix.net) or production?"
|
|
88
|
+
|
|
89
|
+
Pin the choice before anything else; every later command reads it from the workspace.
|
|
88
90
|
|
|
89
91
|
### Canonical Setup Commands (run in terminal after checkpoints)
|
|
90
92
|
```bash
|
|
91
|
-
descix
|
|
92
|
-
descix
|
|
93
|
-
descix init -
|
|
94
|
-
descix app init -a <app> -c <community> # register app on platform
|
|
93
|
+
descix --env dev config init # pin the environment (omit --env dev for production)
|
|
94
|
+
descix login # sign in
|
|
95
|
+
descix app init -a <name> -c <community> # create the app; the platform issues its id
|
|
95
96
|
```
|
|
97
|
+
`app init` prints the app id the platform issued (with `-c` it is `<community>-<name>`). Use that
|
|
98
|
+
id, not the name, as `-a` in every later command.
|
|
96
99
|
|
|
97
100
|
### Checkpoint 4: Content Strategy
|
|
98
101
|
> "Should I create a starter KB doc now, or wait for your domain docs?"
|
|
@@ -317,15 +320,22 @@ Follow Checkpoint 4 from the doctor's `next_steps`:
|
|
|
317
320
|
### If `claude_code_detected` is true (Claude Code environment)
|
|
318
321
|
Also create these files for always-on dynamic alignment:
|
|
319
322
|
|
|
320
|
-
**`.
|
|
323
|
+
**`.mcp.json`** at the project root — Claude Code reads PROJECT MCP servers from this file, not from
|
|
324
|
+
`.claude/settings.json` (`descix quickstart` writes it for you; merge with existing if present):
|
|
321
325
|
```json
|
|
322
326
|
{
|
|
323
327
|
"mcpServers": {
|
|
324
328
|
"descix": {
|
|
325
329
|
"command": "npx",
|
|
326
|
-
"args": ["descix", "mcp-serve"]
|
|
330
|
+
"args": ["-y", "-p", "@descix/cli", "descix", "mcp-serve"]
|
|
327
331
|
}
|
|
328
|
-
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**`.claude/settings.json`** (merge with existing if present):
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
329
339
|
"hooks": {
|
|
330
340
|
"UserPromptSubmit": [
|
|
331
341
|
{
|
package/templates/agent-cline.md
CHANGED
|
@@ -56,15 +56,15 @@ Ask the user at each checkpoint — don't assume:
|
|
|
56
56
|
|
|
57
57
|
1. **Objective:** "Explore existing apps or build something new?"
|
|
58
58
|
2. **Community/App:** Use `find_communities` + `list_apps_for_community`, then ask which to target
|
|
59
|
-
3. **Environment:** "
|
|
59
|
+
3. **Environment:** "Build against DEV (https://dev.descix.net) or production?" Pin it first.
|
|
60
60
|
|
|
61
61
|
Canonical setup commands:
|
|
62
62
|
```bash
|
|
63
|
-
descix
|
|
64
|
-
descix
|
|
65
|
-
descix init -
|
|
66
|
-
descix app init -a <app> -c <community> # register app on platform
|
|
63
|
+
descix --env dev config init # pin the environment (omit --env dev for production)
|
|
64
|
+
descix login # sign in
|
|
65
|
+
descix app init -a <name> -c <community> # create the app; the platform issues its id
|
|
67
66
|
```
|
|
67
|
+
`app init` prints the issued id (`<community>-<name>` with `-c`); use it as `-a` from then on.
|
|
68
68
|
|
|
69
69
|
4. **Content strategy:** "Create starter KB now or wait for your docs?"
|
|
70
70
|
5. **Frontend scope:** "Plain HTML or a framework (React, VitePress)?"
|
|
@@ -56,15 +56,15 @@ Ask the user at each checkpoint — don't assume:
|
|
|
56
56
|
|
|
57
57
|
1. **Objective:** "Explore existing apps or build something new?"
|
|
58
58
|
2. **Community/App:** Use `find_communities` + `list_apps_for_community`, then ask which to target
|
|
59
|
-
3. **Environment:** "
|
|
59
|
+
3. **Environment:** "Build against DEV (https://dev.descix.net) or production?" Pin it first.
|
|
60
60
|
|
|
61
61
|
Canonical setup commands:
|
|
62
62
|
```bash
|
|
63
|
-
descix
|
|
64
|
-
descix
|
|
65
|
-
descix init -
|
|
66
|
-
descix app init -a <app> -c <community> # register app on platform
|
|
63
|
+
descix --env dev config init # pin the environment (omit --env dev for production)
|
|
64
|
+
descix login # sign in
|
|
65
|
+
descix app init -a <name> -c <community> # create the app; the platform issues its id
|
|
67
66
|
```
|
|
67
|
+
`app init` prints the issued id (`<community>-<name>` with `-c`); use it as `-a` from then on.
|
|
68
68
|
|
|
69
69
|
4. **Content strategy:** "Create starter KB now or wait for your docs?"
|
|
70
70
|
5. **Frontend scope:** "Plain HTML or a framework (React, VitePress)?"
|
|
@@ -56,15 +56,15 @@ Ask the user at each checkpoint — don't assume:
|
|
|
56
56
|
|
|
57
57
|
1. **Objective:** "Explore existing apps or build something new?"
|
|
58
58
|
2. **Community/App:** Use `find_communities` + `list_apps_for_community`, then ask which to target
|
|
59
|
-
3. **Environment:** "
|
|
59
|
+
3. **Environment:** "Build against DEV (https://dev.descix.net) or production?" Pin it first.
|
|
60
60
|
|
|
61
61
|
Canonical setup commands:
|
|
62
62
|
```bash
|
|
63
|
-
descix
|
|
64
|
-
descix
|
|
65
|
-
descix init -
|
|
66
|
-
descix app init -a <app> -c <community> # register app on platform
|
|
63
|
+
descix --env dev config init # pin the environment (omit --env dev for production)
|
|
64
|
+
descix login # sign in
|
|
65
|
+
descix app init -a <name> -c <community> # create the app; the platform issues its id
|
|
67
66
|
```
|
|
67
|
+
`app init` prints the issued id (`<community>-<name>` with `-c`); use it as `-a` from then on.
|
|
68
68
|
|
|
69
69
|
4. **Content strategy:** "Create starter KB now or wait for your docs?"
|
|
70
70
|
5. **Frontend scope:** "Plain HTML or a framework (React, VitePress)?"
|