@descix/cli 1.0.20 → 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 +41 -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/deploy-outcome.js +45 -0
- 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
|
@@ -14,6 +14,7 @@ import { requireAuth } from '../lib/auth-guard.js';
|
|
|
14
14
|
import { WorkspaceConfig, unmappedAppMessage, resolveWorkspacePath } from '../lib/workspace-config.js';
|
|
15
15
|
import { CLI_VERSION } from '../lib/cli-version.js';
|
|
16
16
|
import { recordInvocationOrigin } from '../lib/origin.js';
|
|
17
|
+
import { verifyDeployOutcome } from '../lib/deploy-outcome.js';
|
|
17
18
|
// Chat session pointer + the ONE rule for when a dead pointer may be self-healed.
|
|
18
19
|
import {
|
|
19
20
|
getSessionInteractionId,
|
|
@@ -1081,7 +1082,6 @@ appCommand
|
|
|
1081
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.')
|
|
1082
1083
|
.option('--overwrite', 'When creating, overwrite an existing app record intentionally')
|
|
1083
1084
|
.option('--kb <name>', 'Knowledge base name', 'General')
|
|
1084
|
-
.option('-p, --path <dir>', 'Local app directory (default: auto-detected or cwd)')
|
|
1085
1085
|
.action(async (options) => {
|
|
1086
1086
|
try {
|
|
1087
1087
|
const apiClient = new DeSciXApiClient();
|
|
@@ -1500,111 +1500,15 @@ appCommand
|
|
|
1500
1500
|
}
|
|
1501
1501
|
});
|
|
1502
1502
|
|
|
1503
|
-
appCommand
|
|
1504
|
-
.command('set-localpath')
|
|
1505
|
-
.description('Update the local directory path for a mapped app')
|
|
1506
|
-
.requiredOption('-a, --app <app_id>', 'App ID')
|
|
1507
|
-
.requiredOption('-p, --path <dir>', 'New local directory path')
|
|
1508
|
-
.action(async (options) => {
|
|
1509
|
-
try {
|
|
1510
|
-
const appId = options.app;
|
|
1511
|
-
const newPath = options.path;
|
|
1512
|
-
|
|
1513
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
1514
|
-
const wsRoot = workspaceConfig.workspaceRoot || process.cwd();
|
|
1515
|
-
|
|
1516
|
-
// Read the RAW registry entry, never the RESOLVED one. getAppByAppId() resolves the stored
|
|
1517
|
-
// localPath, so on a workspace already carrying a rejected value it throws here — before
|
|
1518
|
-
// any write is attempted. That is what made this verb refuse exactly when it was needed,
|
|
1519
|
-
// while the rejection told the user to fix it with the config verb and never by hand.
|
|
1520
|
-
// This command is about to REPLACE that value; it has no business resolving it.
|
|
1521
|
-
//
|
|
1522
|
-
// Display value only. setLocalPath() is the SINGLE authority on whether the app is mapped
|
|
1523
|
-
// and refuses if it is not — deciding that here as well would be two derivations of one
|
|
1524
|
-
// fact, and they would not even agree: _buildAppIdMap (behind getAppEntry) requires BOTH
|
|
1525
|
-
// appId and localPath, while the live-entry walk matches on appId alone.
|
|
1526
|
-
const oldPath = workspaceConfig.getAppEntry(appId)?.localPath;
|
|
1527
|
-
|
|
1528
|
-
// VALIDATE AT THE WRITE, through the LOADER'S OWN resolver — not a second copy of its rules.
|
|
1529
|
-
// If resolveWorkspacePath would reject this value on the next read, it is rejected now, and
|
|
1530
|
-
// nothing reaches disk. Writing a value the loader will refuse is what bricks a workspace.
|
|
1531
|
-
const absPath = resolveWorkspacePath(wsRoot, newPath, appId);
|
|
1532
|
-
|
|
1533
|
-
// Hard-fail if path doesn't exist or is not a directory — checked at the path the LOADER
|
|
1534
|
-
// will resolve to, not at a CWD-relative path that only exists from where you happened to
|
|
1535
|
-
// be standing when you typed it.
|
|
1536
|
-
let stat;
|
|
1537
|
-
try {
|
|
1538
|
-
stat = await fs.stat(absPath);
|
|
1539
|
-
} catch {
|
|
1540
|
-
throw new Error(
|
|
1541
|
-
`Path does not exist: ${absPath}\n` +
|
|
1542
|
-
` (localPath '${newPath}' resolved against workspace root ${wsRoot})`
|
|
1543
|
-
);
|
|
1544
|
-
}
|
|
1545
|
-
if (!stat.isDirectory()) {
|
|
1546
|
-
throw new Error(
|
|
1547
|
-
`Path is not a directory: ${absPath}\n` +
|
|
1548
|
-
` (localPath '${newPath}' resolved against workspace root ${wsRoot})`
|
|
1549
|
-
);
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// Write through the canonical setter, which covers env.platform AND env.products[]. The
|
|
1553
|
-
// loop this replaces walked env.products ONLY: on the platform app it matched nothing,
|
|
1554
|
-
// wrote nothing, and still printed success — a loud failure turned into a silent lie at
|
|
1555
|
-
// the one entry that matters most.
|
|
1556
|
-
await workspaceConfig.setLocalPath(appId, newPath);
|
|
1557
|
-
|
|
1558
|
-
console.log(chalk.green(`\n✓ ${appId} local path updated`));
|
|
1559
|
-
console.log(chalk.gray(` ${oldPath} → ${newPath}\n`));
|
|
1560
|
-
} catch (error) {
|
|
1561
|
-
console.error(chalk.red(error.message));
|
|
1562
|
-
process.exit(1);
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
1565
|
-
|
|
1566
|
-
appCommand
|
|
1567
|
-
.command('unmap')
|
|
1568
|
-
.description('Remove an app from the local workspace mapping (does not delete Firestore/Pinecone data)')
|
|
1569
|
-
.requiredOption('-a, --app <app_id>', 'App ID')
|
|
1570
|
-
.action(async (options) => {
|
|
1571
|
-
try {
|
|
1572
|
-
const appId = options.app;
|
|
1573
|
-
|
|
1574
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
1575
|
-
const appConfig = workspaceConfig.getAppByAppId(appId);
|
|
1576
|
-
if (!appConfig) {
|
|
1577
|
-
throw new Error(`App '${appId}' is not mapped.`);
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
// Remove from env.products
|
|
1581
|
-
const products = workspaceConfig.env?.products || [];
|
|
1582
|
-
const idx = products.findIndex(p => p.appId === appId || p.app_id === appId);
|
|
1583
|
-
if (idx === -1) {
|
|
1584
|
-
throw new Error(`App '${appId}' is not found in env.products.`);
|
|
1585
|
-
}
|
|
1586
|
-
products.splice(idx, 1);
|
|
1587
|
-
|
|
1588
|
-
const wsRoot = workspaceConfig.workspaceRoot || process.cwd();
|
|
1589
|
-
await workspaceConfig.save(wsRoot);
|
|
1590
|
-
|
|
1591
|
-
console.log(chalk.green(`\n✓ ${appId} removed from workspace mapping`));
|
|
1592
|
-
console.log(chalk.gray(` Pinecone vectors, Firestore docs, and Drive data are not affected.\n`));
|
|
1593
|
-
} catch (error) {
|
|
1594
|
-
console.error(chalk.red(error.message));
|
|
1595
|
-
process.exit(1);
|
|
1596
|
-
}
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
1503
|
// ============ App: set-port (WS-CLI-MESH-ROUTING-GAP) ============
|
|
1600
1504
|
//
|
|
1601
1505
|
// Canonical write path for an app's microservice.port in workspace.json.
|
|
1602
|
-
// `descix microservice init` READS env.
|
|
1506
|
+
// `descix microservice init` READS env.app.microservice.port and hard-fails
|
|
1603
1507
|
// if it is missing; this command is how that port is set, without hand-editing workspace.json.
|
|
1604
1508
|
// Backed by WorkspaceConfig.setMicroservicePort.
|
|
1605
1509
|
appCommand
|
|
1606
1510
|
.command('set-port')
|
|
1607
|
-
.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.')
|
|
1608
1512
|
.requiredOption('-a, --app <app_id>', 'App ID (must be mapped in workspace.json)')
|
|
1609
1513
|
.requiredOption('-p, --port <port>', 'Microservice port number (1-65535), or "n" to remove')
|
|
1610
1514
|
.action(async (options) => {
|
|
@@ -1631,7 +1535,7 @@ appCommand
|
|
|
1631
1535
|
|
|
1632
1536
|
console.log(chalk.green(`\n✓ Microservice port set for ${appId}`));
|
|
1633
1537
|
console.log(chalk.cyan(` Port: ${portNum}`));
|
|
1634
|
-
console.log(chalk.gray(` Written to env.
|
|
1538
|
+
console.log(chalk.gray(` Written to env.app.microservice.port in workspace.json.`));
|
|
1635
1539
|
console.log(chalk.gray(` The gateway routes the microservice on this port; \`descix microservice init -a ${appId}\` can now scaffold it.\n`));
|
|
1636
1540
|
} catch (error) {
|
|
1637
1541
|
console.error(chalk.red(error.message));
|
|
@@ -1655,7 +1559,7 @@ appCommand
|
|
|
1655
1559
|
// Backed by WorkspaceConfig.setStaticSite. It writes ONLY the local workspace.json site.{} slot.
|
|
1656
1560
|
appCommand
|
|
1657
1561
|
.command('set-site')
|
|
1658
|
-
.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.")
|
|
1659
1563
|
.requiredOption('-a, --app <app_id>', 'App ID (must be mapped in workspace.json)')
|
|
1660
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).')
|
|
1661
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.')
|
|
@@ -1714,9 +1618,7 @@ appCommand
|
|
|
1714
1618
|
// Print the RESULTING slot, not just the fields passed: a dev-server write keeps a
|
|
1715
1619
|
// previously declared site.protocol (same mode, new port), and a retained field the
|
|
1716
1620
|
// output does not show is invisible state — the whole defect class this command fixes.
|
|
1717
|
-
const entryNow = workspaceConfig.env?.
|
|
1718
|
-
? workspaceConfig.env.platform
|
|
1719
|
-
: (workspaceConfig.env?.products || []).find(p => p.appId === appId);
|
|
1621
|
+
const entryNow = workspaceConfig.env?.app?.appId === appId ? workspaceConfig.env.app : null;
|
|
1720
1622
|
console.log(chalk.cyan(` site is now: ${JSON.stringify(entryNow?.site)}`));
|
|
1721
1623
|
if (fields.static !== undefined && fields.port === undefined) {
|
|
1722
1624
|
console.log(chalk.gray(` site.port and site.protocol cleared (one active mode per write) — /p/${appId}/ now serves the static dir.`));
|
|
@@ -1725,7 +1627,7 @@ appCommand
|
|
|
1725
1627
|
} else if (fields.static !== undefined && fields.port !== undefined) {
|
|
1726
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).`));
|
|
1727
1629
|
}
|
|
1728
|
-
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`));
|
|
1729
1631
|
} catch (error) {
|
|
1730
1632
|
console.error(chalk.red(error.message));
|
|
1731
1633
|
process.exit(1);
|
|
@@ -1740,9 +1642,9 @@ appCommand
|
|
|
1740
1642
|
// Reuses the SDK resolver resolveAppGatewayUrl() — the same workspace.json read path
|
|
1741
1643
|
// behind buildWorkspaceProducts() that the PWA bakes into __WORKSPACE_PRODUCTS__ — so
|
|
1742
1644
|
// the URL matches what the app store routes to:
|
|
1743
|
-
// -
|
|
1744
|
-
// -
|
|
1745
|
-
// -
|
|
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}
|
|
1746
1648
|
// Hard-fails clearly if the app is unmapped or has no site config.
|
|
1747
1649
|
appCommand
|
|
1748
1650
|
.command('open')
|
|
@@ -2589,6 +2491,28 @@ siteCommand
|
|
|
2589
2491
|
branch: gitStatus.branch
|
|
2590
2492
|
} : null,
|
|
2591
2493
|
preview: options.preview || false
|
|
2494
|
+
}).catch(async (confirmErr) => {
|
|
2495
|
+
// ws-prune-504: a lost confirm RESPONSE is not a failed DEPLOY. Re-read the served
|
|
2496
|
+
// manifest keyed on THIS upload's token and say which it actually is — never "failed"
|
|
2497
|
+
// when the truth is knowable.
|
|
2498
|
+
console.log(chalk.yellow(`\n Confirm response lost (${confirmErr.message}) — reading the deploy record back...`));
|
|
2499
|
+
const verdict = await verifyDeployOutcome(apiClient, {
|
|
2500
|
+
communityId, appId, tokenId: token_id, preview: options.preview || false
|
|
2501
|
+
});
|
|
2502
|
+
if (verdict.outcome === 'confirmed') {
|
|
2503
|
+
console.log(chalk.green(`\n Deploy CONFIRMED — ${verdict.detail}.`));
|
|
2504
|
+
console.log(chalk.gray(` The server may still be finishing the stale-file prune; ` +
|
|
2505
|
+
`'descix site status -c ${communityId} -a ${appId}' shows the deploy, 'descix site list' what is served.`));
|
|
2506
|
+
process.exit(0);
|
|
2507
|
+
}
|
|
2508
|
+
if (verdict.outcome === 'unconfirmed') {
|
|
2509
|
+
console.error(chalk.red(`\n Deploy UNCONFIRMED — ${verdict.detail}.`));
|
|
2510
|
+
console.error(chalk.red(` Nothing was removed from the server. Re-run the upload.`));
|
|
2511
|
+
process.exit(1);
|
|
2512
|
+
}
|
|
2513
|
+
console.error(chalk.red(`\n Deploy outcome UNKNOWN — ${verdict.detail}.`));
|
|
2514
|
+
console.error(chalk.red(` Check 'descix site status -c ${communityId} -a ${appId}' before re-running.`));
|
|
2515
|
+
process.exit(1);
|
|
2592
2516
|
});
|
|
2593
2517
|
|
|
2594
2518
|
const result = confirmResponse.message;
|
|
@@ -2602,6 +2526,10 @@ siteCommand
|
|
|
2602
2526
|
}
|
|
2603
2527
|
console.log(chalk.gray(` Removed from the server: ${pruned.length}`));
|
|
2604
2528
|
pruned.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
2529
|
+
if (result.prune_status === 'partial') {
|
|
2530
|
+
console.log(chalk.yellow(` Prune PARTIAL: ${result.prune_remaining_count} stale file(s) not yet removed (soft budget).`));
|
|
2531
|
+
console.log(chalk.yellow(` ${result.prune_remedy}`));
|
|
2532
|
+
}
|
|
2605
2533
|
console.log(chalk.gray(` Deployed: ${result.deployed_at}`));
|
|
2606
2534
|
if (result.preview) {
|
|
2607
2535
|
console.log(chalk.yellow(` Mode: PREVIEW`));
|
|
@@ -2830,13 +2758,7 @@ microserviceCommand
|
|
|
2830
2758
|
// Hard-fail early if missing — a scaffolded microservice without a known port
|
|
2831
2759
|
// cannot be routed by the gateway and cannot start on a deterministic port.
|
|
2832
2760
|
const env = workspaceConfig.env || {};
|
|
2833
|
-
|
|
2834
|
-
if (env.platform?.appId === ctx.appId) {
|
|
2835
|
-
microservicePort = env.platform?.microservice?.port || null;
|
|
2836
|
-
} else if (Array.isArray(env.products)) {
|
|
2837
|
-
const productEntry = env.products.find(p => p.appId === ctx.appId);
|
|
2838
|
-
microservicePort = productEntry?.microservice?.port || null;
|
|
2839
|
-
}
|
|
2761
|
+
const microservicePort = env.app?.appId === ctx.appId ? (env.app.microservice?.port || null) : null;
|
|
2840
2762
|
|
|
2841
2763
|
if (!microservicePort) {
|
|
2842
2764
|
console.error(chalk.red(`\n❌ App '${ctx.appId}' has no microservice.port in workspace.json.`));
|
|
@@ -4179,7 +4101,7 @@ program
|
|
|
4179
4101
|
// app_ids". `--scope project` was dead on this surface for exactly as long as v2.1 has been
|
|
4180
4102
|
// the format.
|
|
4181
4103
|
//
|
|
4182
|
-
//
|
|
4104
|
+
// v3 carries ONE APP ID (env.app.appId) — no community ids.
|
|
4183
4105
|
// We therefore send app_ids alone and let the server resolve ownership from Products.
|
|
4184
4106
|
// Deriving a community_id by splitting an app_id would violate the platform invariant that
|
|
4185
4107
|
// app ids are OPAQUE to routing and lookups.
|
|
@@ -4199,14 +4121,11 @@ program
|
|
|
4199
4121
|
process.exit(1);
|
|
4200
4122
|
}
|
|
4201
4123
|
const appIds = new Set();
|
|
4202
|
-
if (wsConfig?.env?.
|
|
4203
|
-
for (const product of wsConfig?.env?.products || []) {
|
|
4204
|
-
if (product?.appId) appIds.add(product.appId);
|
|
4205
|
-
}
|
|
4124
|
+
if (wsConfig?.env?.app?.appId) appIds.add(wsConfig.env.app.appId);
|
|
4206
4125
|
// Fail LOUD rather than sending an empty context the server can only reject.
|
|
4207
4126
|
if (appIds.size === 0) {
|
|
4208
|
-
console.error(chalk.red("project scope:
|
|
4209
|
-
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."));
|
|
4210
4129
|
process.exit(1);
|
|
4211
4130
|
}
|
|
4212
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 {
|