@descix/cli 1.0.21 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/descix.js +14 -122
- package/lib/commands/appInit.js +15 -29
- package/lib/commands/auth.js +10 -1
- package/lib/commands/config.js +4 -6
- package/lib/commands/corpus.js +1 -1
- package/lib/commands/health.js +18 -44
- package/lib/commands/init.js +2 -3
- package/lib/commands/kb.js +1 -1
- package/lib/commands/microservice-restart.js +28 -33
- package/lib/commands/status.js +4 -6
- package/lib/verb-invokes.generated.json +2 -3
- package/lib/workspace-config.js +144 -148
- package/lib/workspace-identity.js +6 -13
- package/package.json +2 -2
- package/lib/workspace-utils.js +0 -106
package/bin/descix.js
CHANGED
|
@@ -1082,7 +1082,6 @@ appCommand
|
|
|
1082
1082
|
.option('-s, --short <short_name>', 'SHORT id segment (no hyphens) used when creating; the app_id is composed server-side as {community}-{short}. Defaults to --app.')
|
|
1083
1083
|
.option('--overwrite', 'When creating, overwrite an existing app record intentionally')
|
|
1084
1084
|
.option('--kb <name>', 'Knowledge base name', 'General')
|
|
1085
|
-
.option('-p, --path <dir>', 'Local app directory (default: auto-detected or cwd)')
|
|
1086
1085
|
.action(async (options) => {
|
|
1087
1086
|
try {
|
|
1088
1087
|
const apiClient = new DeSciXApiClient();
|
|
@@ -1501,111 +1500,15 @@ appCommand
|
|
|
1501
1500
|
}
|
|
1502
1501
|
});
|
|
1503
1502
|
|
|
1504
|
-
appCommand
|
|
1505
|
-
.command('set-localpath')
|
|
1506
|
-
.description('Update the local directory path for a mapped app')
|
|
1507
|
-
.requiredOption('-a, --app <app_id>', 'App ID')
|
|
1508
|
-
.requiredOption('-p, --path <dir>', 'New local directory path')
|
|
1509
|
-
.action(async (options) => {
|
|
1510
|
-
try {
|
|
1511
|
-
const appId = options.app;
|
|
1512
|
-
const newPath = options.path;
|
|
1513
|
-
|
|
1514
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
1515
|
-
const wsRoot = workspaceConfig.workspaceRoot || process.cwd();
|
|
1516
|
-
|
|
1517
|
-
// Read the RAW registry entry, never the RESOLVED one. getAppByAppId() resolves the stored
|
|
1518
|
-
// localPath, so on a workspace already carrying a rejected value it throws here — before
|
|
1519
|
-
// any write is attempted. That is what made this verb refuse exactly when it was needed,
|
|
1520
|
-
// while the rejection told the user to fix it with the config verb and never by hand.
|
|
1521
|
-
// This command is about to REPLACE that value; it has no business resolving it.
|
|
1522
|
-
//
|
|
1523
|
-
// Display value only. setLocalPath() is the SINGLE authority on whether the app is mapped
|
|
1524
|
-
// and refuses if it is not — deciding that here as well would be two derivations of one
|
|
1525
|
-
// fact, and they would not even agree: _buildAppIdMap (behind getAppEntry) requires BOTH
|
|
1526
|
-
// appId and localPath, while the live-entry walk matches on appId alone.
|
|
1527
|
-
const oldPath = workspaceConfig.getAppEntry(appId)?.localPath;
|
|
1528
|
-
|
|
1529
|
-
// VALIDATE AT THE WRITE, through the LOADER'S OWN resolver — not a second copy of its rules.
|
|
1530
|
-
// If resolveWorkspacePath would reject this value on the next read, it is rejected now, and
|
|
1531
|
-
// nothing reaches disk. Writing a value the loader will refuse is what bricks a workspace.
|
|
1532
|
-
const absPath = resolveWorkspacePath(wsRoot, newPath, appId);
|
|
1533
|
-
|
|
1534
|
-
// Hard-fail if path doesn't exist or is not a directory — checked at the path the LOADER
|
|
1535
|
-
// will resolve to, not at a CWD-relative path that only exists from where you happened to
|
|
1536
|
-
// be standing when you typed it.
|
|
1537
|
-
let stat;
|
|
1538
|
-
try {
|
|
1539
|
-
stat = await fs.stat(absPath);
|
|
1540
|
-
} catch {
|
|
1541
|
-
throw new Error(
|
|
1542
|
-
`Path does not exist: ${absPath}\n` +
|
|
1543
|
-
` (localPath '${newPath}' resolved against workspace root ${wsRoot})`
|
|
1544
|
-
);
|
|
1545
|
-
}
|
|
1546
|
-
if (!stat.isDirectory()) {
|
|
1547
|
-
throw new Error(
|
|
1548
|
-
`Path is not a directory: ${absPath}\n` +
|
|
1549
|
-
` (localPath '${newPath}' resolved against workspace root ${wsRoot})`
|
|
1550
|
-
);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
// Write through the canonical setter, which covers env.platform AND env.products[]. The
|
|
1554
|
-
// loop this replaces walked env.products ONLY: on the platform app it matched nothing,
|
|
1555
|
-
// wrote nothing, and still printed success — a loud failure turned into a silent lie at
|
|
1556
|
-
// the one entry that matters most.
|
|
1557
|
-
await workspaceConfig.setLocalPath(appId, newPath);
|
|
1558
|
-
|
|
1559
|
-
console.log(chalk.green(`\n✓ ${appId} local path updated`));
|
|
1560
|
-
console.log(chalk.gray(` ${oldPath} → ${newPath}\n`));
|
|
1561
|
-
} catch (error) {
|
|
1562
|
-
console.error(chalk.red(error.message));
|
|
1563
|
-
process.exit(1);
|
|
1564
|
-
}
|
|
1565
|
-
});
|
|
1566
|
-
|
|
1567
|
-
appCommand
|
|
1568
|
-
.command('unmap')
|
|
1569
|
-
.description('Remove an app from the local workspace mapping (does not delete Firestore/Pinecone data)')
|
|
1570
|
-
.requiredOption('-a, --app <app_id>', 'App ID')
|
|
1571
|
-
.action(async (options) => {
|
|
1572
|
-
try {
|
|
1573
|
-
const appId = options.app;
|
|
1574
|
-
|
|
1575
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
1576
|
-
const appConfig = workspaceConfig.getAppByAppId(appId);
|
|
1577
|
-
if (!appConfig) {
|
|
1578
|
-
throw new Error(`App '${appId}' is not mapped.`);
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// Remove from env.products
|
|
1582
|
-
const products = workspaceConfig.env?.products || [];
|
|
1583
|
-
const idx = products.findIndex(p => p.appId === appId || p.app_id === appId);
|
|
1584
|
-
if (idx === -1) {
|
|
1585
|
-
throw new Error(`App '${appId}' is not found in env.products.`);
|
|
1586
|
-
}
|
|
1587
|
-
products.splice(idx, 1);
|
|
1588
|
-
|
|
1589
|
-
const wsRoot = workspaceConfig.workspaceRoot || process.cwd();
|
|
1590
|
-
await workspaceConfig.save(wsRoot);
|
|
1591
|
-
|
|
1592
|
-
console.log(chalk.green(`\n✓ ${appId} removed from workspace mapping`));
|
|
1593
|
-
console.log(chalk.gray(` Pinecone vectors, Firestore docs, and Drive data are not affected.\n`));
|
|
1594
|
-
} catch (error) {
|
|
1595
|
-
console.error(chalk.red(error.message));
|
|
1596
|
-
process.exit(1);
|
|
1597
|
-
}
|
|
1598
|
-
});
|
|
1599
|
-
|
|
1600
1503
|
// ============ App: set-port (WS-CLI-MESH-ROUTING-GAP) ============
|
|
1601
1504
|
//
|
|
1602
1505
|
// Canonical write path for an app's microservice.port in workspace.json.
|
|
1603
|
-
// `descix microservice init` READS env.
|
|
1506
|
+
// `descix microservice init` READS env.app.microservice.port and hard-fails
|
|
1604
1507
|
// if it is missing; this command is how that port is set, without hand-editing workspace.json.
|
|
1605
1508
|
// Backed by WorkspaceConfig.setMicroservicePort.
|
|
1606
1509
|
appCommand
|
|
1607
1510
|
.command('set-port')
|
|
1608
|
-
.description('Set the microservice port for
|
|
1511
|
+
.description('Set the microservice port for this workspace\'s app (writes env.app.microservice.port). Pass "n" to remove.')
|
|
1609
1512
|
.requiredOption('-a, --app <app_id>', 'App ID (must be mapped in workspace.json)')
|
|
1610
1513
|
.requiredOption('-p, --port <port>', 'Microservice port number (1-65535), or "n" to remove')
|
|
1611
1514
|
.action(async (options) => {
|
|
@@ -1632,7 +1535,7 @@ appCommand
|
|
|
1632
1535
|
|
|
1633
1536
|
console.log(chalk.green(`\n✓ Microservice port set for ${appId}`));
|
|
1634
1537
|
console.log(chalk.cyan(` Port: ${portNum}`));
|
|
1635
|
-
console.log(chalk.gray(` Written to env.
|
|
1538
|
+
console.log(chalk.gray(` Written to env.app.microservice.port in workspace.json.`));
|
|
1636
1539
|
console.log(chalk.gray(` The gateway routes the microservice on this port; \`descix microservice init -a ${appId}\` can now scaffold it.\n`));
|
|
1637
1540
|
} catch (error) {
|
|
1638
1541
|
console.error(chalk.red(error.message));
|
|
@@ -1656,7 +1559,7 @@ appCommand
|
|
|
1656
1559
|
// Backed by WorkspaceConfig.setStaticSite. It writes ONLY the local workspace.json site.{} slot.
|
|
1657
1560
|
appCommand
|
|
1658
1561
|
.command('set-site')
|
|
1659
|
-
.description("Set
|
|
1562
|
+
.description("Set this workspace app's site config (env.app.site). One active mode per write: --static <dir> (built site served at /p/<app>/) or --port <n> [--protocol http|https] (dev server proxied at /p/<app>); setting one clears the other. Pass both flags together to keep both — the gateway then serves the dev server (site.port wins). --unset clears site.{} entirely.")
|
|
1660
1563
|
.requiredOption('-a, --app <app_id>', 'App ID (must be mapped in workspace.json)')
|
|
1661
1564
|
.option('--static <path>', 'Static mode: relative dir under the app localPath (e.g. "site"; "." = the localPath itself). Alone, it clears site.port and site.protocol (one active mode per write).')
|
|
1662
1565
|
.option('--port <port>', 'Dev-server mode: proxy /p/<app> to your local dev server on this port (1-65535). Alone, it clears site.static. With --static in the same call, both are kept and the gateway serves the dev server.')
|
|
@@ -1715,9 +1618,7 @@ appCommand
|
|
|
1715
1618
|
// Print the RESULTING slot, not just the fields passed: a dev-server write keeps a
|
|
1716
1619
|
// previously declared site.protocol (same mode, new port), and a retained field the
|
|
1717
1620
|
// output does not show is invisible state — the whole defect class this command fixes.
|
|
1718
|
-
const entryNow = workspaceConfig.env?.
|
|
1719
|
-
? workspaceConfig.env.platform
|
|
1720
|
-
: (workspaceConfig.env?.products || []).find(p => p.appId === appId);
|
|
1621
|
+
const entryNow = workspaceConfig.env?.app?.appId === appId ? workspaceConfig.env.app : null;
|
|
1721
1622
|
console.log(chalk.cyan(` site is now: ${JSON.stringify(entryNow?.site)}`));
|
|
1722
1623
|
if (fields.static !== undefined && fields.port === undefined) {
|
|
1723
1624
|
console.log(chalk.gray(` site.port and site.protocol cleared (one active mode per write) — /p/${appId}/ now serves the static dir.`));
|
|
@@ -1726,7 +1627,7 @@ appCommand
|
|
|
1726
1627
|
} else if (fields.static !== undefined && fields.port !== undefined) {
|
|
1727
1628
|
console.log(chalk.yellow(` Combined write: BOTH modes kept. The gateway serves the DEV SERVER (site.port wins); site.static is used only after you clear site.port (--static alone, or --unset).`));
|
|
1728
1629
|
}
|
|
1729
|
-
console.log(chalk.gray(` Written to env.
|
|
1630
|
+
console.log(chalk.gray(` Written to env.app.site in workspace.json; \`descix serve\` picks it up.\n`));
|
|
1730
1631
|
} catch (error) {
|
|
1731
1632
|
console.error(chalk.red(error.message));
|
|
1732
1633
|
process.exit(1);
|
|
@@ -1741,9 +1642,9 @@ appCommand
|
|
|
1741
1642
|
// Reuses the SDK resolver resolveAppGatewayUrl() — the same workspace.json read path
|
|
1742
1643
|
// behind buildWorkspaceProducts() that the PWA bakes into __WORKSPACE_PRODUCTS__ — so
|
|
1743
1644
|
// the URL matches what the app store routes to:
|
|
1744
|
-
// -
|
|
1745
|
-
// -
|
|
1746
|
-
// -
|
|
1645
|
+
// - the gateway shell → https://localhost:{gatewayPort}/
|
|
1646
|
+
// - an app (static) → https://localhost:{gatewayPort}/p/{appId}
|
|
1647
|
+
// - an app (dev-server) → https://localhost:{gatewayPort}/p/{appId}
|
|
1747
1648
|
// Hard-fails clearly if the app is unmapped or has no site config.
|
|
1748
1649
|
appCommand
|
|
1749
1650
|
.command('open')
|
|
@@ -2857,13 +2758,7 @@ microserviceCommand
|
|
|
2857
2758
|
// Hard-fail early if missing — a scaffolded microservice without a known port
|
|
2858
2759
|
// cannot be routed by the gateway and cannot start on a deterministic port.
|
|
2859
2760
|
const env = workspaceConfig.env || {};
|
|
2860
|
-
|
|
2861
|
-
if (env.platform?.appId === ctx.appId) {
|
|
2862
|
-
microservicePort = env.platform?.microservice?.port || null;
|
|
2863
|
-
} else if (Array.isArray(env.products)) {
|
|
2864
|
-
const productEntry = env.products.find(p => p.appId === ctx.appId);
|
|
2865
|
-
microservicePort = productEntry?.microservice?.port || null;
|
|
2866
|
-
}
|
|
2761
|
+
const microservicePort = env.app?.appId === ctx.appId ? (env.app.microservice?.port || null) : null;
|
|
2867
2762
|
|
|
2868
2763
|
if (!microservicePort) {
|
|
2869
2764
|
console.error(chalk.red(`\n❌ App '${ctx.appId}' has no microservice.port in workspace.json.`));
|
|
@@ -4206,7 +4101,7 @@ program
|
|
|
4206
4101
|
// app_ids". `--scope project` was dead on this surface for exactly as long as v2.1 has been
|
|
4207
4102
|
// the format.
|
|
4208
4103
|
//
|
|
4209
|
-
//
|
|
4104
|
+
// v3 carries ONE APP ID (env.app.appId) — no community ids.
|
|
4210
4105
|
// We therefore send app_ids alone and let the server resolve ownership from Products.
|
|
4211
4106
|
// Deriving a community_id by splitting an app_id would violate the platform invariant that
|
|
4212
4107
|
// app ids are OPAQUE to routing and lookups.
|
|
@@ -4226,14 +4121,11 @@ program
|
|
|
4226
4121
|
process.exit(1);
|
|
4227
4122
|
}
|
|
4228
4123
|
const appIds = new Set();
|
|
4229
|
-
if (wsConfig?.env?.
|
|
4230
|
-
for (const product of wsConfig?.env?.products || []) {
|
|
4231
|
-
if (product?.appId) appIds.add(product.appId);
|
|
4232
|
-
}
|
|
4124
|
+
if (wsConfig?.env?.app?.appId) appIds.add(wsConfig.env.app.appId);
|
|
4233
4125
|
// Fail LOUD rather than sending an empty context the server can only reject.
|
|
4234
4126
|
if (appIds.size === 0) {
|
|
4235
|
-
console.error(chalk.red("project scope:
|
|
4236
|
-
console.error(chalk.gray(" Use --scope entitlements, or
|
|
4127
|
+
console.error(chalk.red("project scope: this workspace holds no app (workspace.json env.app)."));
|
|
4128
|
+
console.error(chalk.gray(" Use --scope entitlements, or run `descix app init` in this repo."));
|
|
4237
4129
|
process.exit(1);
|
|
4238
4130
|
}
|
|
4239
4131
|
project_context = { app_ids: Array.from(appIds) };
|
package/lib/commands/appInit.js
CHANGED
|
@@ -12,27 +12,24 @@
|
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import path from 'path';
|
|
15
|
-
import { WorkspaceConfig,
|
|
15
|
+
import { WorkspaceConfig, assertNoParentWorkspace } from '../workspace-config.js';
|
|
16
16
|
import { refreshCommunityIdentity, printIdentityReceipt } from './communityIdentity.js';
|
|
17
17
|
import { generateAgentFiles } from '../agent-files.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {Object} apiClient an authenticated DeSciXApiClient
|
|
21
|
-
* @param {{app: string, community?: string, short?: string, overwrite?: boolean, kb?: string
|
|
21
|
+
* @param {{app: string, community?: string, short?: string, overwrite?: boolean, kb?: string}} options
|
|
22
22
|
* @returns {Promise<{appId: string, communityId: string, appPath: string, kbId: string}>}
|
|
23
23
|
*/
|
|
24
24
|
export async function runAppInit(apiClient, options) {
|
|
25
25
|
let appId = options.app;
|
|
26
26
|
const kbId = options.kb || 'General';
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
const workspaceConfig = await WorkspaceConfig.tryLoad();
|
|
33
|
-
if (options.path) {
|
|
34
|
-
resolveWorkspacePath(workspaceConfig?.workspaceRoot || process.cwd(), options.path, appId);
|
|
35
|
-
}
|
|
28
|
+
// v3 single-app: THIS directory becomes (or already is) the workspace root. A workspace at an
|
|
29
|
+
// ANCESTOR is a refusal, loud, BEFORE anything is written anywhere — the walk-up used to
|
|
30
|
+
// silently register a nested checkout into the parent workspace (the measured trap).
|
|
31
|
+
await assertNoParentWorkspace(process.cwd());
|
|
32
|
+
const workspaceConfig = await WorkspaceConfig.tryLoad(process.cwd());
|
|
36
33
|
|
|
37
34
|
// Resolve community_id from the Products registry to learn whether the app exists yet.
|
|
38
35
|
let communityId;
|
|
@@ -74,26 +71,15 @@ export async function runAppInit(apiClient, options) {
|
|
|
74
71
|
);
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
// 1. Workspace.json —
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
74
|
+
// 1. Workspace.json — THIS repo root is the workspace root and holds exactly this app (v3).
|
|
75
|
+
// registerApp is idempotent for the same id and REFUSES a different one: a repo that already
|
|
76
|
+
// holds another app errors loudly instead of being silently re-pointed.
|
|
88
77
|
const wsRoot = workspaceConfig?.workspaceRoot || process.cwd();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
appPath = path.resolve(wsRoot, localPath);
|
|
95
|
-
console.log(chalk.gray(` workspace.json updated: ${appId} → ${localPath}`));
|
|
96
|
-
}
|
|
78
|
+
const cfg = workspaceConfig || new WorkspaceConfig({ version: '3.0' }, wsRoot);
|
|
79
|
+
cfg.registerApp(communityId, appId, { kbId });
|
|
80
|
+
await cfg.save(wsRoot);
|
|
81
|
+
const appPath = wsRoot;
|
|
82
|
+
console.log(chalk.gray(` workspace.json: env.app = ${appId} (this repo root)`));
|
|
97
83
|
|
|
98
84
|
// 2. Create app folder structure (site, microservice, assets)
|
|
99
85
|
const assetsDir = path.join(appPath, 'assets');
|
package/lib/commands/auth.js
CHANGED
|
@@ -22,9 +22,18 @@ const POLL_INTERVAL_MS = 3000; // 3 seconds
|
|
|
22
22
|
const MAX_POLL_TIME_MS = 15 * 60 * 1000; // 15 minutes
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Open browser to URL
|
|
25
|
+
* Open browser to URL — INTERACTIVE SESSIONS ONLY.
|
|
26
|
+
*
|
|
27
|
+
* A non-interactive invocation (a spawned test harness, CI, any piped run) must NEVER reach
|
|
28
|
+
* into the operator's live browser: measured 2026-10-10, the CLI test suite repeatedly seized
|
|
29
|
+
* the CEO's active tab because every spawned `quickstart`/`login` walked straight into the
|
|
30
|
+
* device flow's `open`. No TTY on stdout = print the URL, open nothing. DESCIX_NO_BROWSER=1
|
|
31
|
+
* forces the same for interactive runs that want it.
|
|
26
32
|
*/
|
|
27
33
|
async function openBrowser(url) {
|
|
34
|
+
if (!process.stdout.isTTY || process.env.DESCIX_NO_BROWSER === '1') {
|
|
35
|
+
return false; // the caller already prints the URL on a false return
|
|
36
|
+
}
|
|
28
37
|
const platform = process.platform;
|
|
29
38
|
let command;
|
|
30
39
|
|
package/lib/commands/config.js
CHANGED
|
@@ -34,12 +34,10 @@ export async function show() {
|
|
|
34
34
|
console.log(chalk.yellow(` ⚠ This workspace pins ${normalizeOrigin(pinned)} (${environmentNameFor(pinned)}), but ${effective.source} overrides it for this shell.`));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (allApps.length > 0) {
|
|
42
|
-
console.log(chalk.white(` Apps: ${allApps.join(', ')}`));
|
|
37
|
+
// v3 single-app: the one app this workspace holds
|
|
38
|
+
const appId = workspaceConfig.env?.app?.appId;
|
|
39
|
+
if (appId) {
|
|
40
|
+
console.log(chalk.white(` App: ${appId}`));
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
console.log('');
|
package/lib/commands/corpus.js
CHANGED
|
@@ -373,7 +373,7 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
373
373
|
const appConfig = workspaceConfig.getAppByAppId(appId);
|
|
374
374
|
if (!appConfig) {
|
|
375
375
|
spinner.fail(`App not found: ${appId}`);
|
|
376
|
-
throw new Error(`App "${appId}" not
|
|
376
|
+
throw new Error(`App "${appId}" is not this workspace's app (workspace.json env.app).`);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
const appRoot = appConfig.absolutePath;
|
package/lib/commands/health.js
CHANGED
|
@@ -36,7 +36,7 @@ import { promisify } from 'util';
|
|
|
36
36
|
import { localUpstreamOrigin, resolveGatewayPort } from '@descix/app-sdk/dev';
|
|
37
37
|
import https from 'https';
|
|
38
38
|
import http from 'http';
|
|
39
|
-
import { WorkspaceConfig } from '../workspace-config.js';
|
|
39
|
+
import { WorkspaceConfig, resolveLocalApps } from '../workspace-config.js';
|
|
40
40
|
|
|
41
41
|
const execAsync = promisify(exec);
|
|
42
42
|
|
|
@@ -109,47 +109,27 @@ function defaultHttpsProbe(url, timeoutMs = 8000, { insecure = false } = {}) {
|
|
|
109
109
|
*/
|
|
110
110
|
function buildServiceRegistry(wsConfig) {
|
|
111
111
|
const services = [];
|
|
112
|
-
const env = wsConfig.env || {};
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
// The local app set has ONE owner (workspaceApps, v3 fork (b)): this workspace's own
|
|
114
|
+
// env.app plus every repo env.gateway lists, each read from its OWN workspace.json.
|
|
115
|
+
const { apps } = resolveLocalApps(wsConfig);
|
|
116
|
+
for (const app of apps) {
|
|
117
|
+
if (app.microservice?.port) {
|
|
117
118
|
services.push({
|
|
118
|
-
name,
|
|
119
|
-
appId:
|
|
120
|
-
port:
|
|
119
|
+
name: app.appId,
|
|
120
|
+
appId: app.appId,
|
|
121
|
+
port: app.microservice.port,
|
|
121
122
|
type: 'microservice',
|
|
122
|
-
origin: localUpstreamOrigin(
|
|
123
|
+
origin: localUpstreamOrigin(app.microservice, `env.app.microservice (${app.appId})`),
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
|
-
if (
|
|
126
|
+
if (app.site?.port) {
|
|
126
127
|
services.push({
|
|
127
|
-
name: `${
|
|
128
|
-
appId:
|
|
129
|
-
port:
|
|
128
|
+
name: `${app.appId}-site`,
|
|
129
|
+
appId: app.appId,
|
|
130
|
+
port: app.site.port,
|
|
130
131
|
type: 'site',
|
|
131
|
-
origin: localUpstreamOrigin(
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
for (const product of (env.products || [])) {
|
|
137
|
-
if (product.microservice?.port) {
|
|
138
|
-
services.push({
|
|
139
|
-
name: product.appId,
|
|
140
|
-
appId: product.appId,
|
|
141
|
-
port: product.microservice.port,
|
|
142
|
-
type: 'microservice',
|
|
143
|
-
origin: localUpstreamOrigin(product.microservice, `env.products[${product.appId}].microservice`),
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (product.site?.port) {
|
|
147
|
-
services.push({
|
|
148
|
-
name: `${product.appId}-site`,
|
|
149
|
-
appId: product.appId,
|
|
150
|
-
port: product.site.port,
|
|
151
|
-
type: 'site',
|
|
152
|
-
origin: localUpstreamOrigin(product.site, `env.products[${product.appId}].site`),
|
|
132
|
+
origin: localUpstreamOrigin(app.site, `env.app.site (${app.appId})`),
|
|
153
133
|
});
|
|
154
134
|
}
|
|
155
135
|
}
|
|
@@ -279,15 +259,9 @@ async function probeGcloudAuth(execFn) {
|
|
|
279
259
|
* - prod: only descix/egpt/powch (cutover #1 in-scope); raise on out-of-scope filter
|
|
280
260
|
*/
|
|
281
261
|
function resolveAppsForCloudProbe(env, wsConfig, filterAppId) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
for (const product of (wsConfig.env?.products || [])) {
|
|
287
|
-
if (product.appId) allApps.push(product.appId);
|
|
288
|
-
}
|
|
289
|
-
// Dedupe
|
|
290
|
-
const unique = [...new Set(allApps)];
|
|
262
|
+
// The app set has ONE owner (workspaceApps): env.app + every repo env.gateway lists.
|
|
263
|
+
const { apps } = resolveLocalApps(wsConfig);
|
|
264
|
+
const unique = [...new Set(apps.map(a => a.appId))];
|
|
291
265
|
|
|
292
266
|
if (env === 'prod') {
|
|
293
267
|
// PROD scope guard: only in-scope hosts allowed.
|
package/lib/commands/init.js
CHANGED
|
@@ -163,10 +163,9 @@ export async function runInit(apiClient, options = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
console.log(chalk.gray(`\n${existingConfig ? 'Updating' : 'Writing'} .descix/workspace.json...\n`));
|
|
166
|
-
const config = existingConfig || new WorkspaceConfig({ version: '
|
|
166
|
+
const config = existingConfig || new WorkspaceConfig({ version: '3.0', type: 'workspace', env: {} }, projectPath);
|
|
167
167
|
if (existingConfig && options.force && config.env) {
|
|
168
|
-
config.env.
|
|
169
|
-
delete config.env.platform;
|
|
168
|
+
delete config.env.app;
|
|
170
169
|
}
|
|
171
170
|
// NO APP IS REGISTERED HERE. The app id is the platform's: `app init` creates the app and
|
|
172
171
|
// registers the id the server returns (with -c it is composed from the community and the
|
package/lib/commands/kb.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - compare: Show file-level deltas
|
|
12
12
|
*
|
|
13
13
|
* Architecture:
|
|
14
|
-
* - Uses WorkspaceConfig for all path resolution (
|
|
14
|
+
* - Uses WorkspaceConfig for all path resolution (v3 single-app env.app format)
|
|
15
15
|
* - All operations delegate to lib/core/ modules
|
|
16
16
|
* - Supports interactive and unattended (agent) modes
|
|
17
17
|
* - CLI handles UX (prompts, progress, errors)
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Microservice Restart Command (DEV only)
|
|
3
3
|
*
|
|
4
4
|
* Eliminates the "hand-kill + nohup-relaunch" tribal-knowledge workflow:
|
|
5
|
-
* 1. Resolve the service from workspace
|
|
5
|
+
* 1. Resolve the service from the workspace's app set (env.app / env.gateway)
|
|
6
6
|
* (HARD-FAIL on unknown / ambiguous / missing devCommand).
|
|
7
7
|
* 2. Find the running PID via `lsof -iTCP:<port> -sTCP:LISTEN` (or `ps`).
|
|
8
8
|
* 3. SIGTERM → wait up to 5s → SIGKILL.
|
|
9
|
-
* 4. Re-launch the canonical devCommand from `
|
|
9
|
+
* 4. Re-launch the canonical devCommand from the app repo's own `microservice/` dir
|
|
10
10
|
* detached via `setsid + nohup`, redirecting stdout/stderr to a canonical log.
|
|
11
11
|
* 5. Poll until the port is bound again; print new PID + log path.
|
|
12
12
|
* 6. Run `descix health -m <name>` and print the result.
|
|
@@ -27,7 +27,7 @@ import { exec, spawn } from 'child_process';
|
|
|
27
27
|
import { promisify } from 'util';
|
|
28
28
|
import fs from 'fs/promises';
|
|
29
29
|
import path from 'path';
|
|
30
|
-
import { WorkspaceConfig } from '../workspace-config.js';
|
|
30
|
+
import { WorkspaceConfig, resolveLocalApps } from '../workspace-config.js';
|
|
31
31
|
|
|
32
32
|
const execAsync = promisify(exec);
|
|
33
33
|
|
|
@@ -44,28 +44,25 @@ const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
44
44
|
* are restart-eligible (devCommand is the canonical launch signature).
|
|
45
45
|
*
|
|
46
46
|
* @param {WorkspaceConfig} wsConfig
|
|
47
|
-
* @returns {Array<{name,
|
|
47
|
+
* @returns {Array<{name, dir, port, devCommand, microserviceDir}>}
|
|
48
48
|
*/
|
|
49
49
|
export function buildRestartRegistry(wsConfig) {
|
|
50
|
-
const env = wsConfig.env || {};
|
|
51
|
-
const root = wsConfig.workspaceRoot;
|
|
52
50
|
const out = [];
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
// The local app set has ONE owner (workspaceApps, v3 fork (b)): this workspace's own
|
|
53
|
+
// env.app plus every repo env.gateway lists, each read from its OWN workspace.json.
|
|
54
|
+
const { apps } = resolveLocalApps(wsConfig);
|
|
55
|
+
for (const app of apps) {
|
|
56
|
+
const ms = app.microservice;
|
|
57
|
+
if (!ms || !ms.port) continue;
|
|
58
58
|
out.push({
|
|
59
|
-
name:
|
|
60
|
-
|
|
59
|
+
name: app.appId,
|
|
60
|
+
dir: app.dir,
|
|
61
61
|
port: ms.port,
|
|
62
62
|
devCommand: ms.devCommand || null,
|
|
63
|
-
microserviceDir: path.join(
|
|
63
|
+
microserviceDir: path.join(app.dir, 'microservice')
|
|
64
64
|
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
collect(env.platform);
|
|
68
|
-
for (const p of (env.products || [])) collect(p);
|
|
65
|
+
}
|
|
69
66
|
return out;
|
|
70
67
|
}
|
|
71
68
|
|
|
@@ -75,7 +72,7 @@ export function buildRestartRegistry(wsConfig) {
|
|
|
75
72
|
*
|
|
76
73
|
* @param {WorkspaceConfig} wsConfig
|
|
77
74
|
* @param {string} name
|
|
78
|
-
* @returns {{name,
|
|
75
|
+
* @returns {{name, dir, port, devCommand, microserviceDir}}
|
|
79
76
|
*/
|
|
80
77
|
export function resolveService(wsConfig, name) {
|
|
81
78
|
const registry = buildRestartRegistry(wsConfig);
|
|
@@ -84,24 +81,19 @@ export function resolveService(wsConfig, name) {
|
|
|
84
81
|
if (matches.length === 0) {
|
|
85
82
|
const candidates = registry.map((s) => s.name).join(', ') || '(none)';
|
|
86
83
|
throw new Error(
|
|
87
|
-
`Service '${name}' not
|
|
84
|
+
`Service '${name}' is not served by this workspace (env.app / env.gateway).\n` +
|
|
88
85
|
` Restart-eligible services (with microservice.port + microservice.devCommand): ${candidates}\n` +
|
|
89
86
|
` Add an entry to .descix/workspace.json or run 'descix microservice list'.`
|
|
90
87
|
);
|
|
91
88
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
`Service name '${name}' is ambiguous — matches ${matches.length} entries: ` +
|
|
95
|
-
matches.map((m) => `${m.name}@${m.localPath}`).join(', ')
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
89
|
+
// No ambiguity branch: duplicate app ids are refused upstream by the app-set owner
|
|
90
|
+
// (resolveWorkspaceApps), so at most one entry can match here.
|
|
99
91
|
const svc = matches[0];
|
|
100
92
|
if (!svc.devCommand) {
|
|
101
93
|
throw new Error(
|
|
102
94
|
`Service '${name}' has no microservice.devCommand in workspace.json — ` +
|
|
103
95
|
`cannot determine canonical launch signature. ` +
|
|
104
|
-
`Add 'devCommand' under env.
|
|
96
|
+
`Add 'devCommand' under env.app.microservice in that app repo's workspace.json to enable restart.`
|
|
105
97
|
);
|
|
106
98
|
}
|
|
107
99
|
return svc;
|
|
@@ -173,17 +165,20 @@ export async function killPid(pid, opts = {}) {
|
|
|
173
165
|
|
|
174
166
|
/**
|
|
175
167
|
* Resolve the canonical log path for a restart.
|
|
176
|
-
*
|
|
168
|
+
* <app repo root>/.descix/logs/<service>-<ts>.log
|
|
169
|
+
* The app repo root is the entry's own `dir` (workspaceApps): under v3 each app lives in its
|
|
170
|
+
* own repo, so there is no workspace-wide apps/<community>/ tree to hang logs under.
|
|
177
171
|
* Falls back to /tmp/<service>-restart-<ts>.log if the canonical dir
|
|
178
172
|
* cannot be created.
|
|
173
|
+
*
|
|
174
|
+
* @param {{name: string, dir: string}} svc - a restart-registry entry
|
|
179
175
|
*/
|
|
180
|
-
export async function resolveLogPath(
|
|
176
|
+
export async function resolveLogPath(svc, opts = {}) {
|
|
181
177
|
const deps = opts.deps || {};
|
|
182
178
|
const fsImpl = deps.fs || fs;
|
|
183
179
|
const ts = (opts.timestamp || new Date().toISOString()).replace(/[:.]/g, '-');
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const canonicalDir = path.join(root, 'apps', community, serviceName, '.descix', 'logs');
|
|
180
|
+
const serviceName = svc.name;
|
|
181
|
+
const canonicalDir = path.join(svc.dir, '.descix', 'logs');
|
|
187
182
|
try {
|
|
188
183
|
await fsImpl.mkdir(canonicalDir, { recursive: true });
|
|
189
184
|
return {
|
|
@@ -303,7 +298,7 @@ export async function restartMicroservice(args) {
|
|
|
303
298
|
}
|
|
304
299
|
|
|
305
300
|
// Step 2: resolve log path.
|
|
306
|
-
const logRes = await resolveLogPath(
|
|
301
|
+
const logRes = await resolveLogPath(svc, { deps });
|
|
307
302
|
if (logRes.fallback) {
|
|
308
303
|
log(chalk.yellow(` Canonical log dir unavailable (${logRes.fallbackReason}); using ${logRes.path}`));
|
|
309
304
|
} else {
|
package/lib/commands/status.js
CHANGED
|
@@ -159,12 +159,10 @@ export async function runStatus(options = {}) {
|
|
|
159
159
|
console.log(`Config File: ${path.join(workspaceRoot, '.descix', 'workspace.json')}`);
|
|
160
160
|
|
|
161
161
|
try {
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (allApps.length > 0) {
|
|
167
|
-
console.log(`Local Apps: ${allApps.join(', ')}`);
|
|
162
|
+
// v3 single-app: the one app this workspace holds
|
|
163
|
+
const wsAppId = workspaceConfig.env?.app?.appId;
|
|
164
|
+
if (wsAppId) {
|
|
165
|
+
console.log(`App: ${wsAppId}`);
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
// WHAT THE WORKSPACE FILE ITSELF SAYS — deliberately NOT a resolved origin. The resolved
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"app set-listed": [
|
|
35
35
|
"update_app"
|
|
36
36
|
],
|
|
37
|
-
"app set-localpath": [],
|
|
38
37
|
"app set-name": [
|
|
39
38
|
"update_app"
|
|
40
39
|
],
|
|
@@ -49,7 +48,6 @@
|
|
|
49
48
|
"app sync-assets": [
|
|
50
49
|
"sync_app_assets"
|
|
51
50
|
],
|
|
52
|
-
"app unmap": [],
|
|
53
51
|
"buy chains": [],
|
|
54
52
|
"buy poll": [
|
|
55
53
|
"get_quote_status"
|
|
@@ -218,7 +216,8 @@
|
|
|
218
216
|
"site upload": [
|
|
219
217
|
"confirm_site_deploy",
|
|
220
218
|
"get_site_build_config",
|
|
221
|
-
"get_site_deploy_token"
|
|
219
|
+
"get_site_deploy_token",
|
|
220
|
+
"get_site_manifest"
|
|
222
221
|
],
|
|
223
222
|
"stats": [
|
|
224
223
|
"get_user_community_stats"
|
package/lib/workspace-config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { ENV_ORIGINS } from '@descix/app-sdk/dev';
|
|
3
|
+
import { ENV_ORIGINS, resolveWorkspaceApps } from '@descix/app-sdk/dev';
|
|
4
4
|
import { resolveOrigin } from './origin.js';
|
|
5
5
|
// The one canonical KB-sync surface, from its owner. The example in requireContext() is a remedy
|
|
6
6
|
// a stuck developer will copy verbatim, so it must resolve to the live verb rather than a literal
|
|
@@ -22,8 +22,9 @@ import { npxCommand } from './invocation.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export function unmappedAppMessage(appId) {
|
|
24
24
|
return (
|
|
25
|
-
`App "${appId}" is not
|
|
26
|
-
'
|
|
25
|
+
`App "${appId}" is not this workspace's app (workspace.json env.app). ` +
|
|
26
|
+
'A v3 workspace is single-app: run commands for another app from ITS repo root, ' +
|
|
27
|
+
'or run `descix app init -a <id>` in a fresh repo to create its workspace.'
|
|
27
28
|
);
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -55,6 +56,55 @@ export function resolveWorkspacePath(workspaceRoot, localPath, label = 'app') {
|
|
|
55
56
|
return path.resolve(workspaceRoot, localPath);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The local app set of a loaded workspace — THE one CLI-side entry to resolveWorkspaceApps.
|
|
61
|
+
* Every command that enumerates local apps (health, microservice restart) goes through here, so
|
|
62
|
+
* a config that carries no workspaceRoot FAILS LOUD in one place instead of each caller quietly
|
|
63
|
+
* substituting the cwd and enumerating a different workspace than the one it was handed.
|
|
64
|
+
*
|
|
65
|
+
* @param {WorkspaceConfig} wsConfig
|
|
66
|
+
* @returns {{ shellApp: Object|null, apps: Object[] }} see resolveWorkspaceApps
|
|
67
|
+
*/
|
|
68
|
+
export function resolveLocalApps(wsConfig) {
|
|
69
|
+
if (!wsConfig?.workspaceRoot) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'Cannot enumerate the workspace apps: the workspace config carries no workspaceRoot. ' +
|
|
72
|
+
'Load it with WorkspaceConfig.load() (which sets it) rather than constructing it bare.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return resolveWorkspaceApps(wsConfig.workspaceRoot);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `descix app init` runs where the APP REPO is — its root becomes the workspace root. A nested
|
|
80
|
+
* checkout used to WALK UP, find a parent workspace and silently register into it (the measured
|
|
81
|
+
* trap: an app repo cloned inside another workspace mapped itself into the parent's registry).
|
|
82
|
+
* Under v3 single-app there is nothing to register into: a workspace found at an ANCESTOR of the
|
|
83
|
+
* init directory is a refusal, loud, before anything is written anywhere.
|
|
84
|
+
*
|
|
85
|
+
* A workspace AT the directory itself is fine — that is the re-init case.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} dir - the directory `app init` is about to make a workspace root
|
|
88
|
+
* @throws when a workspace.json exists at a strict ancestor of dir
|
|
89
|
+
*/
|
|
90
|
+
export async function assertNoParentWorkspace(dir = process.cwd()) {
|
|
91
|
+
const resolved = path.resolve(dir);
|
|
92
|
+
const found = await WorkspaceConfig.findWorkspaceRoot(resolved);
|
|
93
|
+
if (found && path.resolve(found) !== resolved) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`This directory is nested inside another DeSciX workspace.\n` +
|
|
96
|
+
` Here: ${resolved}\n` +
|
|
97
|
+
` Parent workspace: ${found} (.descix/workspace.json)\n` +
|
|
98
|
+
`\n` +
|
|
99
|
+
`A v3 workspace is single-app: the app repo's own root is its workspace root, and\n` +
|
|
100
|
+
`"descix app init" refuses to run inside someone else's workspace rather than\n` +
|
|
101
|
+
`silently registering this checkout into it.\n` +
|
|
102
|
+
`Move this checkout OUT of ${found} (sibling checkouts, one gateway config, is the\n` +
|
|
103
|
+
`supported local layout), then run "descix app init" again from the repo root.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
58
108
|
/**
|
|
59
109
|
* The ONE text for "there is no workspace here". It is correct advice for exactly one state —
|
|
60
110
|
* the file does not exist — and it is a lie for every other state, which is why it now has a
|
|
@@ -166,7 +216,7 @@ export class WorkspaceConfig {
|
|
|
166
216
|
this.environment = config.environment;
|
|
167
217
|
this.directoryMappings = config.directoryMappings || {};
|
|
168
218
|
|
|
169
|
-
// app_id →
|
|
219
|
+
// app_id → config map from env.app (v3 single-app)
|
|
170
220
|
this._appIdToConfig = this._buildAppIdMap(config);
|
|
171
221
|
|
|
172
222
|
// Environment URLs for descix-serve gateway routing
|
|
@@ -177,50 +227,39 @@ export class WorkspaceConfig {
|
|
|
177
227
|
}
|
|
178
228
|
|
|
179
229
|
/**
|
|
180
|
-
* Build app_id → config map from env.
|
|
181
|
-
*
|
|
230
|
+
* Build the app_id → config map from env.app (v3 single-app).
|
|
231
|
+
*
|
|
232
|
+
* v3 holds exactly ONE app and its localPath is implicitly '.': the app repo's root IS the
|
|
233
|
+
* workspace root. The map shape survives (one entry) so every reader keyed on appId keeps
|
|
234
|
+
* its contract; the retired v2.1 multi-app walk (env.platform + env.products) is refused by
|
|
235
|
+
* load() and has no reader here any more.
|
|
236
|
+
*
|
|
182
237
|
* @param {Object} config - Parsed workspace.json
|
|
183
238
|
* @returns {Object} Map of appId -> { localPath, communityId?, kbId }
|
|
184
239
|
*/
|
|
185
240
|
_buildAppIdMap(config) {
|
|
186
241
|
const map = {};
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
localPath: env.platform.localPath,
|
|
194
|
-
communityId: env.platform.communityId || null,
|
|
195
|
-
kbId: env.platform.kbId || 'General'
|
|
242
|
+
const app = config.env?.app;
|
|
243
|
+
if (app?.appId) {
|
|
244
|
+
map[app.appId] = {
|
|
245
|
+
localPath: '.',
|
|
246
|
+
communityId: app.communityId || null,
|
|
247
|
+
kbId: app.kbId || 'General'
|
|
196
248
|
};
|
|
197
249
|
}
|
|
198
|
-
|
|
199
|
-
// env.products array (e.g. powch -> DeSciX_Powch)
|
|
200
|
-
const products = Array.isArray(env.products) ? env.products : [];
|
|
201
|
-
for (const p of products) {
|
|
202
|
-
if (p?.appId && p?.localPath) {
|
|
203
|
-
map[p.appId] = {
|
|
204
|
-
localPath: p.localPath,
|
|
205
|
-
communityId: p.communityId || null,
|
|
206
|
-
kbId: p.kbId || 'General'
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
250
|
return map;
|
|
212
251
|
}
|
|
213
252
|
|
|
214
253
|
/**
|
|
215
254
|
* Get app config by app_id (Unified Registry - app_id is globally unique)
|
|
216
|
-
* Resolves from env.
|
|
255
|
+
* Resolves from env.app (v3 single-app)
|
|
217
256
|
* @param {string} appId - App/product identifier
|
|
218
257
|
* @returns {Object|null} { localPath, absolutePath?, communityId?, kbId } or null
|
|
219
258
|
*/
|
|
220
259
|
getAppByAppId(appId) {
|
|
221
260
|
if (!appId || !this.workspaceRoot) return null;
|
|
222
261
|
|
|
223
|
-
// env.
|
|
262
|
+
// env.app (v3 single-app)
|
|
224
263
|
const fromEnv = this._appIdToConfig[appId];
|
|
225
264
|
if (fromEnv) {
|
|
226
265
|
const absPath = resolveWorkspacePath(this.workspaceRoot, fromEnv.localPath, appId);
|
|
@@ -235,87 +274,29 @@ export class WorkspaceConfig {
|
|
|
235
274
|
return null;
|
|
236
275
|
}
|
|
237
276
|
|
|
238
|
-
|
|
239
|
-
* The RAW registry entry for an app — `localPath` exactly as workspace.json carries it, with NO
|
|
240
|
-
* path resolution and therefore no possibility of throwing on a bad stored value.
|
|
241
|
-
*
|
|
242
|
-
* This exists for exactly one caller shape: a command that is about to REPLACE an app's
|
|
243
|
-
* localPath. Such a command must not resolve the OUTGOING value. getAppByAppId() resolves, and
|
|
244
|
-
* resolving the value you are on your way to overwrite is what made `descix app set-localpath`
|
|
245
|
-
* unusable precisely when it was needed — a workspace already carrying a rejected localPath
|
|
246
|
-
* could not be repaired by the one verb whose job is repairing it, while the rejection itself
|
|
247
|
-
* forbade hand-editing. The verb was the prescribed remedy and the verb refused.
|
|
248
|
-
*
|
|
249
|
-
* It reads the SAME map getAppByAppId reads (_buildAppIdMap is still the one owner of "what is
|
|
250
|
-
* mapped"); it simply stops short of resolution. Do NOT use it to obtain a usable path — take
|
|
251
|
-
* getAppByAppId().absolutePath for that, so the loader's rejection still runs.
|
|
252
|
-
*
|
|
253
|
-
* @param {string} appId - App/product identifier
|
|
254
|
-
* @returns {{ localPath: string, communityId: string|null, kbId: string }|null}
|
|
255
|
-
*/
|
|
256
|
-
getAppEntry(appId) {
|
|
257
|
-
if (!appId) return null;
|
|
258
|
-
return this._appIdToConfig?.[appId] || null;
|
|
259
|
-
}
|
|
277
|
+
|
|
260
278
|
|
|
261
279
|
/**
|
|
262
280
|
* THE ONE OWNER of "find the LIVE env entry for this app, or hard-fail".
|
|
263
281
|
*
|
|
264
282
|
* Live means the actual object inside this.env — mutate it and save() persists the mutation.
|
|
265
|
-
* getAppByAppId()
|
|
283
|
+
* getAppByAppId() returns copies and is useless for writing.
|
|
266
284
|
*
|
|
267
|
-
* The one lookup every entry setter (setMicroservicePort, setStaticSite
|
|
268
|
-
*
|
|
269
|
-
* still reports success.
|
|
285
|
+
* The one lookup every entry setter (setMicroservicePort, setStaticSite) uses. In v3 the only
|
|
286
|
+
* live entry is env.app; any other id hard-fails with the canonical unmapped message.
|
|
270
287
|
*
|
|
271
288
|
* @param {string} appId - App identifier
|
|
272
|
-
* @returns {Object} the live env.
|
|
273
|
-
* @throws if appId is not
|
|
289
|
+
* @returns {Object} the live env.app entry
|
|
290
|
+
* @throws if appId is not THE app
|
|
274
291
|
*/
|
|
275
292
|
_liveEnvEntry(appId) {
|
|
276
|
-
|
|
277
|
-
if (this.env?.platform?.appId === appId) {
|
|
278
|
-
entry = this.env.platform;
|
|
279
|
-
} else if (Array.isArray(this.env?.products)) {
|
|
280
|
-
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
281
|
-
}
|
|
293
|
+
const entry = this.env?.app?.appId === appId ? this.env.app : null;
|
|
282
294
|
if (!entry) {
|
|
283
295
|
throw new Error(unmappedAppMessage(appId));
|
|
284
296
|
}
|
|
285
297
|
return entry;
|
|
286
298
|
}
|
|
287
299
|
|
|
288
|
-
/**
|
|
289
|
-
* Update an app's localPath in env.platform or env.products[].
|
|
290
|
-
*
|
|
291
|
-
* The canonical write path for localPath. Backs `descix app set-localpath`.
|
|
292
|
-
*
|
|
293
|
-
* localPath MUST be workspace-root-relative. It is validated here through
|
|
294
|
-
* resolveWorkspacePath — the LOADER'S OWN resolver, not a second copy of its rules — so a
|
|
295
|
-
* value the loader will refuse on the next read is refused now, before anything is written.
|
|
296
|
-
* Writing a value the loader rejects is what bricked workspaces: every subsequent command
|
|
297
|
-
* threw, including the repair verbs, while the rejection forbade hand-editing.
|
|
298
|
-
*
|
|
299
|
-
* Persists via save() — same auto-save pattern as its siblings.
|
|
300
|
-
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
301
|
-
*
|
|
302
|
-
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
303
|
-
* @param {string} localPath - New path, relative to the workspace root
|
|
304
|
-
* @returns {Promise<string>} Path to saved config (from save())
|
|
305
|
-
*/
|
|
306
|
-
async setLocalPath(appId, localPath) {
|
|
307
|
-
if (!appId) throw new Error('appId is required');
|
|
308
|
-
if (!localPath) throw new Error('localPath is required');
|
|
309
|
-
|
|
310
|
-
const entry = this._liveEnvEntry(appId);
|
|
311
|
-
|
|
312
|
-
// Validate through the loader's own resolver. Throws on an absolute path.
|
|
313
|
-
resolveWorkspacePath(this.workspaceRoot, localPath, appId);
|
|
314
|
-
|
|
315
|
-
entry.localPath = localPath;
|
|
316
|
-
return this.save();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
300
|
/**
|
|
320
301
|
* Get the absolute path to an app's site/ directory
|
|
321
302
|
* @param {string} appId - App identifier
|
|
@@ -437,6 +418,28 @@ export class WorkspaceConfig {
|
|
|
437
418
|
);
|
|
438
419
|
}
|
|
439
420
|
|
|
421
|
+
// v2.1 MULTI-APP hard-error: env.platform / env.products are the retired mapping model —
|
|
422
|
+
// one workspace registering OTHER repos' apps by localPath. v3 is single-app (CEO
|
|
423
|
+
// 2026-10-09; fork (b) 2026-10-10): the app repo's root IS the workspace root, and local
|
|
424
|
+
// multi-app dev is sibling checkouts behind one gateway config. Same posture as the v1
|
|
425
|
+
// refusal above: loud, non-destructive, the remedy is one command per app repo.
|
|
426
|
+
if (parsed.env && (parsed.env.platform || parsed.env.products)) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
'v2.1 multi-app workspace format is not supported. Migrate to v3 (single-app).\n' +
|
|
429
|
+
` File: ${configPath}\n` +
|
|
430
|
+
'\n' +
|
|
431
|
+
'This file is still on disk and has NOT been modified. v3 holds exactly ONE app\n' +
|
|
432
|
+
'(env.app) and the app repo\'s own root is the workspace root. To migrate:\n' +
|
|
433
|
+
' • In EACH app repo this file mapped, run from that repo\'s root:\n' +
|
|
434
|
+
' descix config init --env <dev|demo|prod> # pins env.apiUrl\n' +
|
|
435
|
+
' descix app init -a <app_id> # writes the v3 env.app block\n' +
|
|
436
|
+
' • A gateway hosting several local apps lists their repo roots in ITS OWN\n' +
|
|
437
|
+
' workspace.json under env.gateway.appRepos (fork (b): sibling checkouts, one\n' +
|
|
438
|
+
' gateway config).\n' +
|
|
439
|
+
moveAsideRemedy(configPath, 'descix app init')
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
440
443
|
// A v2.1 file carrying a TOP-LEVEL `apiUrl` is refused by name. That key is retired: no
|
|
441
444
|
// reader consumes it (the origin owner resolves `env.apiUrl` only) and save() never writes
|
|
442
445
|
// it, so a developer who set it by hand would be silently ignored on every invocation —
|
|
@@ -480,8 +483,8 @@ export class WorkspaceConfig {
|
|
|
480
483
|
}
|
|
481
484
|
|
|
482
485
|
/**
|
|
483
|
-
* Save workspace configuration in
|
|
484
|
-
*
|
|
486
|
+
* Save workspace configuration in v3 (single-app) format: env.app, never
|
|
487
|
+
* env.platform / env.products.
|
|
485
488
|
*
|
|
486
489
|
* PRESERVES KEYS IT DOES NOT UNDERSTAND. This method used to serialize a fixed field list from
|
|
487
490
|
* memory over the whole file, so ANY top-level key it did not itself enumerate was silently
|
|
@@ -508,7 +511,7 @@ export class WorkspaceConfig {
|
|
|
508
511
|
|
|
509
512
|
this.workspaceRoot = root;
|
|
510
513
|
|
|
511
|
-
// Build env block for
|
|
514
|
+
// Build env block for v3 output
|
|
512
515
|
const envBlock = (this.env && Object.keys(this.env).length > 0) ? this.env : null;
|
|
513
516
|
|
|
514
517
|
// The keys this writer OWNS. Everything else on disk is carried through untouched.
|
|
@@ -537,7 +540,7 @@ export class WorkspaceConfig {
|
|
|
537
540
|
|
|
538
541
|
const configData = {
|
|
539
542
|
...carried,
|
|
540
|
-
version: '
|
|
543
|
+
version: '3.0',
|
|
541
544
|
workspaceRoot: path.resolve(root),
|
|
542
545
|
type: this.type,
|
|
543
546
|
};
|
|
@@ -552,54 +555,47 @@ export class WorkspaceConfig {
|
|
|
552
555
|
// ============ App Registration Methods ============
|
|
553
556
|
|
|
554
557
|
/**
|
|
555
|
-
* Register
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
+
* Register THE app of this v3 workspace (env.app).
|
|
559
|
+
*
|
|
560
|
+
* Single-app is the FORMAT, not a convention: a workspace that already holds a different app
|
|
561
|
+
* refuses loudly — there is no second slot to put one in, and silently replacing env.app
|
|
562
|
+
* would re-point every path-derived surface (site/, microservice/, manifests) at a different
|
|
563
|
+
* platform record. Re-registering the SAME app is idempotent (the `app init` re-run case) and
|
|
564
|
+
* may update kbId / communityId.
|
|
565
|
+
*
|
|
566
|
+
* localPath is GONE as an input: the app repo's root is the workspace root, always ('.').
|
|
567
|
+
*
|
|
568
|
+
* communityId is STORED, not just validated. It was previously accepted as a parameter and
|
|
569
|
+
* silently discarded, so `descix init -c egpt` wrote no trace of `egpt` anywhere — and
|
|
570
|
+
* `workspace-identity.js` (this entry's reader) then had no community to report, which is
|
|
571
|
+
* how the generated agent files named nobody's community. Writer and reader are a pair.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} communityId - The app's community (server-authoritative value, mirrored here)
|
|
558
574
|
* @param {string} appId - App identifier (globally unique)
|
|
559
|
-
* @param {Object} appConfig -
|
|
560
|
-
* @param {string} appConfig.localPath - Local folder path relative to workspaceRoot
|
|
561
|
-
* @param {string} [appConfig.kbId] - Default knowledge base ID
|
|
575
|
+
* @param {Object} [appConfig] - { kbId? }
|
|
562
576
|
* @returns {boolean} Success status
|
|
563
577
|
*/
|
|
564
|
-
registerApp(communityId, appId, appConfig) {
|
|
565
|
-
if (!appId
|
|
566
|
-
throw new Error('appId
|
|
578
|
+
registerApp(communityId, appId, appConfig = {}) {
|
|
579
|
+
if (!appId) {
|
|
580
|
+
throw new Error('appId is required');
|
|
567
581
|
}
|
|
568
582
|
|
|
569
|
-
// Validate through THE LOADER'S OWN RESOLVER — the same owner setLocalPath consumes, never a
|
|
570
|
-
// second copy of its rules. Three writers persisted a localPath and only ONE validated it, so
|
|
571
|
-
// `descix app init -p /abs` wrote an absolute value, created directories at that arbitrary
|
|
572
|
-
// absolute location, printed success, and bricked the workspace on the NEXT read — the loader
|
|
573
|
-
// refused the value for every command including the repair verbs, while the refusal forbade
|
|
574
|
-
// hand-editing. A path this CLI accepts must be honoured, or refused before anything is written.
|
|
575
|
-
resolveWorkspacePath(this.workspaceRoot, appConfig.localPath, appId);
|
|
576
|
-
|
|
577
583
|
if (!this.env) this.env = {};
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const entry = { appId, localPath: appConfig.localPath, kbId: appConfig.kbId || 'General' };
|
|
586
|
-
if (communityId) entry.communityId = communityId;
|
|
587
|
-
|
|
588
|
-
if (this.env.platform?.appId === appId) {
|
|
589
|
-
this.env.platform.localPath = appConfig.localPath;
|
|
590
|
-
if (appConfig.kbId) this.env.platform.kbId = appConfig.kbId;
|
|
591
|
-
if (communityId) this.env.platform.communityId = communityId;
|
|
592
|
-
} else {
|
|
593
|
-
const idx = this.env.products.findIndex(p => p.appId === appId);
|
|
594
|
-
if (idx >= 0) {
|
|
595
|
-
this.env.products[idx] = { ...this.env.products[idx], ...entry };
|
|
596
|
-
} else {
|
|
597
|
-
this.env.products.push(entry);
|
|
598
|
-
}
|
|
584
|
+
const existing = this.env.app;
|
|
585
|
+
if (existing?.appId && existing.appId !== appId) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`This workspace already holds app '${existing.appId}' — a v3 workspace is single-app.\n` +
|
|
588
|
+
`To work on '${appId}', clone or create its OWN repo and run "descix app init" from\n` +
|
|
589
|
+
`that repo's root (sibling checkouts, one gateway config, is the supported local layout).`
|
|
590
|
+
);
|
|
599
591
|
}
|
|
600
592
|
|
|
593
|
+
const entry = { ...(existing || {}), appId, kbId: appConfig.kbId || existing?.kbId || 'General' };
|
|
594
|
+
if (communityId) entry.communityId = communityId;
|
|
595
|
+
this.env.app = entry;
|
|
596
|
+
|
|
601
597
|
this._appIdToConfig = this._buildAppIdMap({ env: this.env, workspaceRoot: this.workspaceRoot });
|
|
602
|
-
this.version = '
|
|
598
|
+
this.version = '3.0';
|
|
603
599
|
return true;
|
|
604
600
|
}
|
|
605
601
|
|
|
@@ -648,7 +644,7 @@ export class WorkspaceConfig {
|
|
|
648
644
|
const cwd = path.resolve(startDir);
|
|
649
645
|
const wsRoot = this.workspaceRoot ? path.resolve(this.workspaceRoot) : null;
|
|
650
646
|
|
|
651
|
-
// env.
|
|
647
|
+
// env.app (v3 single-app): at most one entry
|
|
652
648
|
if (wsRoot) {
|
|
653
649
|
for (const [appId, cfg] of Object.entries(this._appIdToConfig || {})) {
|
|
654
650
|
const appPath = resolveWorkspacePath(wsRoot, cfg.localPath, appId);
|
|
@@ -776,7 +772,7 @@ export class WorkspaceConfig {
|
|
|
776
772
|
throw new Error(
|
|
777
773
|
`"${dottedKey}" is not a workspace-level env key.\n` +
|
|
778
774
|
` Settable here: ${ALLOWED.join(', ')}\n` +
|
|
779
|
-
' Per-app keys have their own verbs: descix app set-site / set-port
|
|
775
|
+
' Per-app keys have their own verbs: descix app set-site / set-port.'
|
|
780
776
|
);
|
|
781
777
|
}
|
|
782
778
|
|
|
@@ -806,7 +802,7 @@ export class WorkspaceConfig {
|
|
|
806
802
|
}
|
|
807
803
|
|
|
808
804
|
/**
|
|
809
|
-
* Update
|
|
805
|
+
* Update the app's microservice.port on env.app (v3 single-app).
|
|
810
806
|
*
|
|
811
807
|
* Operates on the entry's microservice.{} slot.
|
|
812
808
|
* This is the canonical write path for the microservice port that `descix microservice init`
|
|
@@ -815,9 +811,9 @@ export class WorkspaceConfig {
|
|
|
815
811
|
*
|
|
816
812
|
* Pass `null` to remove microservice.port; if microservice.{} becomes empty, it is also deleted.
|
|
817
813
|
* Persists via save() at the end — same auto-save pattern as setEnvironment().
|
|
818
|
-
* Hard-fails if appId is not
|
|
814
|
+
* Hard-fails if appId is not THE app (env.app).
|
|
819
815
|
*
|
|
820
|
-
* @param {string} appId - App identifier (must
|
|
816
|
+
* @param {string} appId - App identifier (must be env.app.appId)
|
|
821
817
|
* @param {number|string|null} port - Port number to set, or null to remove microservice.port
|
|
822
818
|
* @returns {Promise<string>} Path to saved config (from save())
|
|
823
819
|
*/
|
|
@@ -843,7 +839,7 @@ export class WorkspaceConfig {
|
|
|
843
839
|
}
|
|
844
840
|
|
|
845
841
|
/**
|
|
846
|
-
* Update
|
|
842
|
+
* Update the app's site config on env.app (v3 single-app).
|
|
847
843
|
*
|
|
848
844
|
* Parallel to setMicroservicePort(), but operates on the entry's site.{} slot. This is the
|
|
849
845
|
* canonical write path for site.static — the relative path the dev gateway's
|
|
@@ -867,9 +863,9 @@ export class WorkspaceConfig {
|
|
|
867
863
|
* Pass null for a field to remove it (what `--unset` does for all three). Removals clear
|
|
868
864
|
* only the named field. If site.{} becomes empty after removals it is deleted.
|
|
869
865
|
* Persists via save() at the end — same auto-save pattern as setMicroservicePort().
|
|
870
|
-
* Hard-fails if appId is not
|
|
866
|
+
* Hard-fails if appId is not THE app (env.app).
|
|
871
867
|
*
|
|
872
|
-
* @param {string} appId - App identifier (must
|
|
868
|
+
* @param {string} appId - App identifier (must be env.app.appId)
|
|
873
869
|
* @param {Object} fields - Fields to set on site.{}
|
|
874
870
|
* @param {string|null} [fields.static] - Relative static-site path to set, or null to remove site.static
|
|
875
871
|
* @param {number|string|null} [fields.port] - Site dev-server port to set, or null to remove site.port
|
|
@@ -1006,9 +1002,9 @@ export class WorkspaceConfig {
|
|
|
1006
1002
|
validate() {
|
|
1007
1003
|
const errors = [];
|
|
1008
1004
|
|
|
1009
|
-
//
|
|
1005
|
+
// v3 validation: must have env block
|
|
1010
1006
|
if (!this.env || Object.keys(this.env).length === 0) {
|
|
1011
|
-
errors.push('Missing env block — workspace.json must be
|
|
1007
|
+
errors.push('Missing env block — workspace.json must be v3 format with env.app');
|
|
1012
1008
|
}
|
|
1013
1009
|
|
|
1014
1010
|
return {
|
|
@@ -24,10 +24,9 @@ import { resolveOrigin } from './origin.js';
|
|
|
24
24
|
/**
|
|
25
25
|
* Read the workspace's identity from a parsed `.descix/workspace.json`.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* wrong again, so the two are commented as a pair.
|
|
27
|
+
* v3 single-app: the one key the writer (`WorkspaceConfig.registerApp()`) writes is `env.app`,
|
|
28
|
+
* so that is the one key read here — writer and reader are a pair. The legacy `defaultContext`
|
|
29
|
+
* survives as the last resort for files written before the env block existed.
|
|
31
30
|
*
|
|
32
31
|
* @param {object} config - the PARSED workspace.json (not a WorkspaceConfig instance, so that
|
|
33
32
|
* `agent-files.js` can stay usable from the VS Code extension where it
|
|
@@ -37,22 +36,16 @@ import { resolveOrigin } from './origin.js';
|
|
|
37
36
|
*/
|
|
38
37
|
export function readIdentity(config = {}) {
|
|
39
38
|
const env = config.env || {};
|
|
40
|
-
const
|
|
41
|
-
const products = Array.isArray(env.products) ? env.products : [];
|
|
42
|
-
// The first registered product is the workspace's app when there is no platform app. A
|
|
43
|
-
// workspace with several products has no single identity, and `init` only ever writes one.
|
|
44
|
-
const product = products[0] || {};
|
|
39
|
+
const app = env.app || {};
|
|
45
40
|
const legacy = config.defaultContext || {};
|
|
46
41
|
|
|
47
42
|
const appId =
|
|
48
|
-
|
|
49
|
-
product.appId ||
|
|
43
|
+
app.appId ||
|
|
50
44
|
legacy.appId ||
|
|
51
45
|
null;
|
|
52
46
|
|
|
53
47
|
const communityId =
|
|
54
|
-
|
|
55
|
-
product.communityId ||
|
|
48
|
+
app.communityId ||
|
|
56
49
|
legacy.communityId ||
|
|
57
50
|
config.primaryCommunity ||
|
|
58
51
|
null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descix/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "DeSciX",
|
|
31
31
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@descix/app-sdk": "^0.
|
|
33
|
+
"@descix/app-sdk": "^0.3.0",
|
|
34
34
|
"@descix/sdk": "^3.0.0",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
36
36
|
"axios": "^1.7.9",
|
package/lib/workspace-utils.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace Utilities
|
|
3
|
-
*
|
|
4
|
-
* Helper functions for workspace mode detection and path resolution.
|
|
5
|
-
* v2.1 workspace format only (env.platform / env.products).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import path from 'path';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Detect workspace mode from workspace.json configuration (v2.1 format)
|
|
12
|
-
* @param {Object} workspace - Parsed workspace.json
|
|
13
|
-
* @returns {'single_app' | 'single_community' | 'multi_community'}
|
|
14
|
-
*/
|
|
15
|
-
export function detectWorkspaceMode(workspace) {
|
|
16
|
-
// Explicit mode declaration
|
|
17
|
-
if (workspace.mode) {
|
|
18
|
-
return workspace.mode;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// v2.1 format: infer from env.platform + env.products
|
|
22
|
-
if (workspace.env?.platform) {
|
|
23
|
-
const productCount = Array.isArray(workspace.env.products) ? workspace.env.products.length : 0;
|
|
24
|
-
// Platform + products = multi-community workspace
|
|
25
|
-
return productCount > 0 ? 'multi_community' : 'single_app';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Fallback for empty/initialized workspace using defaultContext
|
|
29
|
-
if (workspace.defaultContext?.communityId && workspace.defaultContext?.appId) {
|
|
30
|
-
return 'single_app';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
throw new Error('Unable to determine workspace mode from configuration. Expected v2.1 format (env.platform / env.products).');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Resolve local path to Drive path based on workspace mode
|
|
38
|
-
* @param {Object} workspace - Workspace configuration
|
|
39
|
-
* @param {string} localPath - Path relative to workspace root
|
|
40
|
-
* @returns {string} Path relative to user's base_folder_id
|
|
41
|
-
*/
|
|
42
|
-
export function resolveToDrivePath(workspace, localPath) {
|
|
43
|
-
const mode = detectWorkspaceMode(workspace);
|
|
44
|
-
|
|
45
|
-
// Normalize local path (remove leading ./)
|
|
46
|
-
const normalizedLocal = localPath.replace(/^\.\//, '');
|
|
47
|
-
|
|
48
|
-
switch (mode) {
|
|
49
|
-
case 'single_app': {
|
|
50
|
-
const communityId = workspace.defaultContext?.communityId || workspace.env?.platform?.communityId;
|
|
51
|
-
const appId = workspace.defaultContext?.appId || workspace.env?.platform?.appId;
|
|
52
|
-
|
|
53
|
-
if (!communityId || !appId) {
|
|
54
|
-
throw new Error('Missing community_id or app_id for single_app mode path resolution');
|
|
55
|
-
}
|
|
56
|
-
return `${communityId}/${appId}/${normalizedLocal}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
case 'single_community': {
|
|
60
|
-
const commId = workspace.defaultContext?.communityId || workspace.env?.platform?.communityId;
|
|
61
|
-
if (!commId) {
|
|
62
|
-
throw new Error('Missing community_id for single_community mode path resolution');
|
|
63
|
-
}
|
|
64
|
-
return `${commId}/${normalizedLocal}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
case 'multi_community':
|
|
68
|
-
// Workspace root = base folder
|
|
69
|
-
return normalizedLocal;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Resolve Drive path to local path based on workspace mode
|
|
75
|
-
* @param {Object} workspace - Workspace configuration
|
|
76
|
-
* @param {string} drivePath - Path relative to user's base_folder_id
|
|
77
|
-
* @returns {string} Path relative to workspace root
|
|
78
|
-
*/
|
|
79
|
-
export function resolveToLocalPath(workspace, drivePath) {
|
|
80
|
-
const mode = detectWorkspaceMode(workspace);
|
|
81
|
-
|
|
82
|
-
switch (mode) {
|
|
83
|
-
case 'single_app': {
|
|
84
|
-
const communityId = workspace.defaultContext?.communityId || workspace.env?.platform?.communityId;
|
|
85
|
-
const appId = workspace.defaultContext?.appId || workspace.env?.platform?.appId;
|
|
86
|
-
|
|
87
|
-
const appPrefix = `${communityId}/${appId}/`;
|
|
88
|
-
if (drivePath.startsWith(appPrefix)) {
|
|
89
|
-
return './' + drivePath.slice(appPrefix.length);
|
|
90
|
-
}
|
|
91
|
-
throw new Error(`Drive path '${drivePath}' does not match workspace app '${appId}'`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
case 'single_community': {
|
|
95
|
-
const commId = workspace.defaultContext?.communityId || workspace.env?.platform?.communityId;
|
|
96
|
-
const commPrefix = `${commId}/`;
|
|
97
|
-
if (drivePath.startsWith(commPrefix)) {
|
|
98
|
-
return './' + drivePath.slice(commPrefix.length);
|
|
99
|
-
}
|
|
100
|
-
throw new Error(`Drive path '${drivePath}' does not match workspace community '${commId}'`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
case 'multi_community':
|
|
104
|
-
return './' + drivePath;
|
|
105
|
-
}
|
|
106
|
-
}
|