@descix/cli 1.0.21 → 2.0.1
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 +167 -153
- 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"
|