@descix/cli 1.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.
Files changed (94) hide show
  1. package/README.md +95 -0
  2. package/SERVICE_README_sdk.md +399 -0
  3. package/agent-assets/guides/README.md +61 -0
  4. package/agent-assets/guides/dynamic-site-pattern.md +125 -0
  5. package/agent-assets/guides/knowledge-only-pattern.md +93 -0
  6. package/agent-assets/guides/microservice-pattern.md +152 -0
  7. package/agent-assets/guides/static-site-pattern.md +111 -0
  8. package/agent-assets/instructions/app-asset-validation.md +477 -0
  9. package/agent-assets/instructions/onboarding-flow.md +408 -0
  10. package/agent-assets/instructions/standard-folder-structure.md +125 -0
  11. package/agent-assets/reference/app-creation.md +381 -0
  12. package/agent-assets/reference/architecture.md +406 -0
  13. package/agent-assets/reference/local-dev.md +138 -0
  14. package/agent-assets/reference/mcp-integration.md +424 -0
  15. package/agent-assets/reference/workspace-config.md +415 -0
  16. package/agent-assets/rules/descix_mcp.template.mdc +243 -0
  17. package/bin/descix.js +3930 -0
  18. package/bin/mcp-server.js +382 -0
  19. package/descix-cli-1.0.0.tgz +0 -0
  20. package/lib/agent-files.js +162 -0
  21. package/lib/api-client.js +458 -0
  22. package/lib/auth-guard.js +67 -0
  23. package/lib/commands/app-wizard.js +627 -0
  24. package/lib/commands/auth.js +407 -0
  25. package/lib/commands/buy.js +207 -0
  26. package/lib/commands/clone.js +91 -0
  27. package/lib/commands/config.js +155 -0
  28. package/lib/commands/doctor.js +277 -0
  29. package/lib/commands/folder.js +291 -0
  30. package/lib/commands/init.js +257 -0
  31. package/lib/commands/kb.js +654 -0
  32. package/lib/commands/mcp.js +273 -0
  33. package/lib/commands/serve.js +12 -0
  34. package/lib/commands/status.js +239 -0
  35. package/lib/commands/update.js +653 -0
  36. package/lib/core/Chunker.js +610 -0
  37. package/lib/core/Hydrator.js +801 -0
  38. package/lib/core/PathContext.js +335 -0
  39. package/lib/core/Syncer.js +310 -0
  40. package/lib/core/index.js +32 -0
  41. package/lib/global-config.js +86 -0
  42. package/lib/google-storage-adc.js +511 -0
  43. package/lib/wallet-file.js +232 -0
  44. package/lib/wizard/setup.js +502 -0
  45. package/lib/workspace-config.js +712 -0
  46. package/lib/workspace-utils.js +126 -0
  47. package/package.json +41 -0
  48. package/templates/agent-claude.md +226 -0
  49. package/templates/agent-cline.md +133 -0
  50. package/templates/agent-copilot.md +133 -0
  51. package/templates/agent-cursor.md +133 -0
  52. package/templates/descix_mcp.mdc +122 -0
  53. package/templates/drive/README.md +42 -0
  54. package/templates/drive/app/assets/app_description.md +13 -0
  55. package/templates/drive/app/assets/icon.png +0 -0
  56. package/templates/drive/app/assets/system_instructions.md +17 -0
  57. package/templates/drive/app/kb/General/README.md +25 -0
  58. package/templates/drive/community/Apps/.gitkeep +4 -0
  59. package/templates/drive/community/community_assets/community_description.md +15 -0
  60. package/templates/drive/community/community_assets/icon.png +0 -0
  61. package/templates/drive_templates/README.md +104 -0
  62. package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
  63. package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
  64. package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
  65. package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
  66. package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
  67. package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
  68. package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
  69. package/templates/scaffolds/README.md +62 -0
  70. package/templates/scaffolds/microservice/Dockerfile +19 -0
  71. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
  72. package/templates/scaffolds/microservice/app.js +50 -0
  73. package/templates/scaffolds/microservice/app.yaml +8 -0
  74. package/templates/scaffolds/microservice/defaults-config.json +5 -0
  75. package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
  76. package/templates/scaffolds/microservice/manifest.json +49 -0
  77. package/templates/scaffolds/microservice/package-lock.json +2645 -0
  78. package/templates/scaffolds/microservice/package.json +29 -0
  79. package/templates/scaffolds/microservice/scripts/register.js +55 -0
  80. package/templates/scaffolds/microservice/services/apiFront.js +75 -0
  81. package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
  82. package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
  83. package/templates/scaffolds/microservice/services/utils.js +247 -0
  84. package/templates/scaffolds/microservice/setup-schema.json +18 -0
  85. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
  86. package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
  87. package/templates/scaffolds/site/README.md +51 -0
  88. package/templates/scaffolds/site/app.js +22 -0
  89. package/templates/scaffolds/site/index.html +42 -0
  90. package/templates/scaffolds/site/styles.css +123 -0
  91. package/test-hydration.js +150 -0
  92. package/vendor/mcp/transports/http-sse-transport.js +196 -0
  93. package/vendor/mcp/transports/index.js +15 -0
  94. package/vendor/mcp/transports/stdio-transport.js +118 -0
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@descix/service-starter",
3
+ "version": "1.0.0",
4
+ "description": "DeSciX Add-on Service Starter Template (MCP-Native)",
5
+ "main": "app.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node app.js",
9
+ "dev": "nodemon app.js",
10
+ "register": "node scripts/register.js",
11
+ "gen-jwt-secret": "node -e 'console.log(require(\"crypto\").randomBytes(32).toString(\"hex\"))'"
12
+ },
13
+ "dependencies": {
14
+ "@descix/cloud-core": "^1.0.0",
15
+ "@google-cloud/firestore": "^7.1.0",
16
+ "@google-cloud/secret-manager": "^5.0.1",
17
+ "@google-cloud/storage": "^7.7.0",
18
+ "axios": "^1.6.2",
19
+ "cors": "^2.8.5",
20
+ "dotenv": "^16.3.1",
21
+ "express": "^4.18.2",
22
+ "google-auth-library": "^9.4.1",
23
+ "@descix/sdk": "^1.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "nodemon": "^3.0.2"
27
+ },
28
+ "license": "@Daita_DeSci_Community_License_v1/Daita_DeSci_Community_License_v1.pdf"
29
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Service Registration Script
3
+ *
4
+ * Registers the service manifest with DeSciX Cloud.
5
+ * Usage: npm run register
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { Firestore } from '@google-cloud/firestore';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ // Load Manifest
17
+ const manifestPath = path.resolve(__dirname, '../manifest.json');
18
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
19
+
20
+ async function register() {
21
+ console.log(`[Register] Registering service: ${manifest.service.name}`);
22
+
23
+ // Connect to Firestore (uses ADC)
24
+ // Assumes we are running in a context with access to the Core project
25
+ // OR we might need to hit an admin API endpoint on Core if we don't have direct DB access.
26
+ // For this template, we'll assume direct DB access (common for internal services)
27
+ // or provide instructions to copy output to Core.
28
+
29
+ // OPTION: We could check if we have CREDENTIALS, if not, output the JSON
30
+ // for the user to paste into the Core admin console.
31
+
32
+ try {
33
+ const firestore = new Firestore();
34
+ const docRef = firestore.collection('ServiceManifests').doc(manifest.service.name);
35
+
36
+ await docRef.set({
37
+ ...manifest,
38
+ registeredAt: new Date().toISOString(),
39
+ updatedAt: new Date().toISOString(),
40
+ status: 'active'
41
+ });
42
+
43
+ console.log(`[Register] SUCCESS: Registered ${manifest.service.name}`);
44
+ console.log(`[Register] Domain: ${manifest.service.domain}`);
45
+ console.log(`[Register] Commands: ${Object.keys(manifest.commands).join(', ')}`);
46
+
47
+ } catch (error) {
48
+ console.error('[Register] FAILED:', error.message);
49
+ console.log('\n[Tip] If you don\'t have direct Firestore access, create a document in the "ServiceManifests" collection with this ID and content:');
50
+ console.log(`ID: ${manifest.service.name}`);
51
+ console.log(JSON.stringify(manifest, null, 2));
52
+ }
53
+ }
54
+
55
+ register();
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Service API Router
3
+ *
4
+ * Routes incoming JSON commands to handlers.
5
+ * Expects `_descix` context injected by the Core Broker.
6
+ */
7
+
8
+ import express from 'express';
9
+ import { createRequire } from 'module';
10
+ const require = createRequire(import.meta.url);
11
+
12
+ const apiRouter = express.Router();
13
+
14
+ // Command Registry
15
+ const commandRegistry = {
16
+ // Dynamic loading will happen in the handler
17
+ 'sample_hello': 'sampleCommands.js',
18
+ 'sample_analyze': 'sampleCommands.js'
19
+ };
20
+
21
+ class ServiceCommandHandler {
22
+ static async invoke(command, params) {
23
+ // 1. Check registry
24
+ const fileName = commandRegistry[command];
25
+ if (!fileName) {
26
+ throw new Error(`Unknown command: ${command}`);
27
+ }
28
+
29
+ // 2. Load module (simple dynamic import)
30
+ try {
31
+ const module = await import(`./commandHandlers/${fileName}`);
32
+ const handler = module.commands[command];
33
+
34
+ if (!handler) {
35
+ throw new Error(`Handler not found for ${command} in ${fileName}`);
36
+ }
37
+
38
+ // 3. Execute
39
+ // We pass params directly. The handler is responsible for using _descix context.
40
+ return await handler(params);
41
+
42
+ } catch (error) {
43
+ console.error(`[CommandHandler] Error invoking ${command}:`, error);
44
+ throw error;
45
+ }
46
+ }
47
+ }
48
+
49
+ // Main Route
50
+ apiRouter.post('/:command', async (req, res) => {
51
+ const { command } = req.params;
52
+ const params = req.body;
53
+
54
+ // The Core Broker injects this context
55
+ const context = params._descix;
56
+
57
+ if (!context) {
58
+ // In production, you might reject requests without context
59
+ // In dev, we might allow direct calls
60
+ console.warn(`[ApiFront] Warning: Request to ${command} missing _descix context`);
61
+ }
62
+
63
+ try {
64
+ console.log(`[ApiFront] Received command: ${command}`);
65
+ const result = await ServiceCommandHandler.invoke(command, params);
66
+ res.json(result);
67
+ } catch (error) {
68
+ res.status(500).json({
69
+ status: 'ERROR',
70
+ message: error.message
71
+ });
72
+ }
73
+ });
74
+
75
+ export default apiRouter;
@@ -0,0 +1,57 @@
1
+ import { mcpClient } from '../mcpClient.js';
2
+
3
+ export const commands = {
4
+ /**
5
+ * Simple echo command
6
+ */
7
+ sample_hello: async (params) => {
8
+ const { message, _descix } = params;
9
+
10
+ return {
11
+ status: 'OK',
12
+ message: `Echo: ${message}`,
13
+ user: _descix?.user?.email || 'Guest'
14
+ };
15
+ },
16
+
17
+ /**
18
+ * Demonstrates Loopback:
19
+ * 1. Receives a query
20
+ * 2. Calls Core Tool (query_knowledge_base) via MCP Client
21
+ * 3. Returns combined result
22
+ */
23
+ sample_analyze: async (params) => {
24
+ const { query, _descix } = params;
25
+
26
+ if (!_descix?.user) {
27
+ throw new Error('Authentication required for analysis');
28
+ }
29
+
30
+ console.log(`[SampleService] Analyzing '${query}' for user ${_descix.user.id}`);
31
+
32
+ try {
33
+ // CALL CORE TOOL: query_knowledge_base
34
+ // This goes Service -> Cloud -> Vector DB -> Cloud -> Service
35
+ const ragResult = await mcpClient.callTool('query_knowledge_base', {
36
+ community_id: 'descix',
37
+ app_id: 'docs',
38
+ kb_id: 'sdk',
39
+ query: query
40
+ }, _descix);
41
+
42
+ return {
43
+ status: 'OK',
44
+ analysis: `Analyzed query: ${query}`,
45
+ rag_findings: ragResult || 'No results found',
46
+ timestamp: new Date().toISOString()
47
+ };
48
+ } catch (error) {
49
+ console.error('[SampleService] RAG lookup failed:', error);
50
+ return {
51
+ status: 'ERROR',
52
+ message: 'Analysis failed due to upstream error',
53
+ details: error.message
54
+ };
55
+ }
56
+ }
57
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * DeSciX MCP Loopback Client
3
+ *
4
+ * Enables the service to call Core Tools (e.g. RAG, Purchase) or other Service Tools
5
+ * via the Federated MCP Broker (DeSciX_Cloud).
6
+ */
7
+
8
+ import axios from 'axios';
9
+ import { utils } from './utils.js';
10
+ import { Signer } from '@descix/sdk'; // Assuming descix-sdk is available in the container
11
+
12
+ class McpClient {
13
+ constructor() {
14
+ // Initialize signer if key is available
15
+ if (utils.SERVICE_KEY) {
16
+ this.signer = new Signer(utils.SERVICE_KEY.privateKey, utils.SERVICE_KEY.slotId);
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Call a tool via the Core Broker
22
+ * @param {string} toolName - Name of the tool to call (e.g., 'query_knowledge_base')
23
+ * @param {Object} args - Tool arguments
24
+ * @param {Object} context - User context object (from _descix) to pass identity
25
+ * @returns {Promise<any>} - Tool result
26
+ */
27
+ async callTool(toolName, args, context) {
28
+ if (!utils.CORE_API_URL) {
29
+ throw new Error('CORE_API_URL not configured');
30
+ }
31
+
32
+ // Construct the payload simulating a PWA or internal request
33
+ const payload = {
34
+ command: toolName,
35
+ params: {
36
+ ...args,
37
+ // Critical: Pass the original user context back to Core
38
+ // The Core trusts this service (authenticated via Service Secret or VPC)
39
+ // In this simplified template, we pass the user ID we received
40
+ user_id: context?.user?.id || utils.OWNER_USER_ID,
41
+ // If we have owner credentials, use them as fallback for auth
42
+ wallet_address: utils.OWNER_WALLET_ADDRESS,
43
+ signature: utils.OWNER_SIGNATURE
44
+ }
45
+ };
46
+
47
+ try {
48
+ console.log(`[McpClient] Calling ${toolName} on ${utils.CORE_API_URL}`);
49
+
50
+ // Get headers (Signature or Legacy Secret)
51
+ const headers = {
52
+ 'Content-Type': 'application/json'
53
+ };
54
+
55
+ if (this.signer) {
56
+ // Sign the payload string
57
+ const signatureHeaders = this.signer.getHeaders(JSON.stringify(payload));
58
+ Object.assign(headers, signatureHeaders);
59
+ } else if (utils.SERVICE_SECRET) {
60
+ // Fallback to legacy secret
61
+ headers['Authorization'] = `Bearer ${utils.SERVICE_SECRET}`;
62
+ }
63
+
64
+ const response = await axios.post(utils.CORE_API_URL, payload, { headers });
65
+
66
+ if (response.data?.status === 'OK') {
67
+ return response.data.message;
68
+ } else {
69
+ throw new Error(response.data?.message || 'Unknown error from Core');
70
+ }
71
+ } catch (error) {
72
+ console.error(`[McpClient] Error calling ${toolName}:`, error.message);
73
+ throw error;
74
+ }
75
+ }
76
+ }
77
+
78
+ export const mcpClient = new McpClient();
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Service Configuration Bootstrap
3
+ *
4
+ * STANDARD BOOTSTRAP PATTERN:
5
+ * - .env: Only bootstrap keys (CONFIG_SECRET_NAME, CONFIG_SECRET_VERSION, DEPLOY_ENV)
6
+ * - defaults-config.json: Instance defaults (checked in)
7
+ * - Secret Manager: Global config
8
+ * - dev-overrides.json: Local dev overrides (gitignored)
9
+ */
10
+
11
+ import { fileURLToPath } from 'url';
12
+ import { GoogleAuth } from 'google-auth-library';
13
+ import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
14
+ import dotenv from 'dotenv';
15
+ import path from 'path';
16
+ import fs from 'fs';
17
+ import { createRequire } from 'module';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+ const require = createRequire(import.meta.url);
22
+
23
+ class ServiceConfig {
24
+ constructor() {
25
+ if (ServiceConfig.instance) return ServiceConfig.instance;
26
+
27
+ this.__appDir = path.resolve(__dirname, '../');
28
+
29
+ // 1. Load Bootstrap .env
30
+ if (process.env.DEPLOY_ENV !== 'production') {
31
+ dotenv.config({ path: path.resolve(this.__appDir, '.env') });
32
+ }
33
+
34
+ this.CONFIG_SECRET_NAME = process.env.CONFIG_SECRET_NAME || null;
35
+ this.CONFIG_SECRET_VERSION = process.env.CONFIG_SECRET_VERSION || null;
36
+ this.DEPLOY_ENV = process.env.DEPLOY_ENV || null;
37
+
38
+ // 2. Initialize Config Keys (All NULL - no hardcoded defaults)
39
+ this.DEBUG_LOCAL = null;
40
+ this.LOCAL_PORT = null;
41
+ this.CORE_API_URL = null; // URL for DeSciX Core (for loopback)
42
+ this.SERVICE_SECRET = null; // Secret for authenticating loopback calls (Legacy)
43
+ this.SERVICE_KEY = null; // Service Delegate Key (New)
44
+
45
+ // Owner Credentials (for loopback API calls)
46
+ this.OWNER_WALLET_ADDRESS = null;
47
+ this.OWNER_SIGNATURE = null;
48
+ this.OWNER_USER_ID = null;
49
+
50
+ this.GOOGLE_PROJECT_ID = null;
51
+ this.secretClient = null;
52
+ this.PORT = process.env.PORT || null;
53
+
54
+ // Load instance defaults immediately
55
+ this._loadDefaults();
56
+
57
+ ServiceConfig.instance = this;
58
+ }
59
+
60
+ _mergeConfig(config) {
61
+ for (const [key, value] of Object.entries(config)) {
62
+ if (key.startsWith('_')) continue;
63
+ // Allow dynamic expansion (undefined) or overwrite (null/value)
64
+ this[key] = value;
65
+ }
66
+ }
67
+
68
+ _loadDefaults() {
69
+ const defaultsPath = path.resolve(this.__appDir, 'defaults-config.json');
70
+ if (fs.existsSync(defaultsPath)) {
71
+ try {
72
+ this._mergeConfig(require(defaultsPath));
73
+ console.log('[Config] Loaded defaults-config.json');
74
+ } catch (e) {
75
+ console.error('[Config] Error loading defaults-config.json:', e.message);
76
+ }
77
+ }
78
+ }
79
+
80
+ _loadDevOverrides() {
81
+ if (this.DEPLOY_ENV !== 'dev') return;
82
+ const overridesPath = path.resolve(this.__appDir, 'dev-overrides.json');
83
+ if (fs.existsSync(overridesPath)) {
84
+ try {
85
+ this._mergeConfig(require(overridesPath));
86
+ console.log('[Config] Loaded dev-overrides.json');
87
+ } catch (e) {
88
+ console.error('[Config] Error loading dev-overrides.json:', e.message);
89
+ }
90
+ }
91
+ }
92
+
93
+ async accessSecretVersion(secretName, version) {
94
+ if (!version) throw new Error('[Config] CRITICAL: CONFIG_SECRET_VERSION not set');
95
+ if (!this.secretClient) this.secretClient = new SecretManagerServiceClient();
96
+
97
+ const name = `projects/${this.GOOGLE_PROJECT_ID}/secrets/${secretName}/versions/${version}`;
98
+ const [response] = await this.secretClient.accessSecretVersion({ name });
99
+ return response.payload.data.toString('utf8');
100
+ }
101
+
102
+ async _registerWithWorkspace() {
103
+ if (this.DEPLOY_ENV !== 'dev' && !this.DEBUG_LOCAL) return;
104
+
105
+ if (this.DEBUG_LOCAL) {
106
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
107
+ }
108
+
109
+ let communityId = null;
110
+ let appId = null;
111
+ let workspacePath = null;
112
+
113
+ // Walk up from the service directory to find .descix/workspace.json
114
+ let searchDir = this.__appDir;
115
+ for (let i = 0; i < 10; i++) {
116
+ const candidate = path.resolve(searchDir, '.descix/workspace.json');
117
+ if (fs.existsSync(candidate)) {
118
+ workspacePath = candidate;
119
+ break;
120
+ }
121
+ const parent = path.dirname(searchDir);
122
+ if (parent === searchDir) break;
123
+ searchDir = parent;
124
+ }
125
+
126
+ // Detect context from manifest.json
127
+ try {
128
+ const manifestPath = path.resolve(this.__appDir, 'manifest.json');
129
+ if (fs.existsSync(manifestPath)) {
130
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
131
+ communityId = manifest.service?.community_id || manifest.community_id;
132
+ appId = manifest.service?.app_id || manifest.app_id;
133
+ }
134
+ } catch (e) {
135
+ console.warn('[Config] Failed to detect app context from manifest:', e.message);
136
+ }
137
+
138
+ if (!communityId || !appId) {
139
+ console.log('[Config] No communityId/appId — skipping workspace registration');
140
+ return;
141
+ }
142
+
143
+ if (!workspacePath) {
144
+ console.log('[Config] No .descix/workspace.json found — skipping workspace registration');
145
+ return;
146
+ }
147
+
148
+ // Write service port into workspace.json for descix-serve to pick up
149
+ try {
150
+ const workspace = JSON.parse(fs.readFileSync(workspacePath, 'utf-8'));
151
+ if (!workspace.communities) workspace.communities = {};
152
+ if (!workspace.communities[communityId]) workspace.communities[communityId] = { apps: {} };
153
+ if (!workspace.communities[communityId].apps) workspace.communities[communityId].apps = {};
154
+ if (!workspace.communities[communityId].apps[appId]) workspace.communities[communityId].apps[appId] = {};
155
+
156
+ const appEntry = workspace.communities[communityId].apps[appId];
157
+ if (!appEntry.service) appEntry.service = {};
158
+ appEntry.service.port = parseInt(this.PORT, 10);
159
+
160
+ fs.writeFileSync(workspacePath, JSON.stringify(workspace, null, 2), 'utf-8');
161
+ console.log(`[Config] Registered ${communityId}/${appId} service port ${this.PORT} in workspace.json`);
162
+
163
+ this._workspacePath = workspacePath;
164
+ this._registeredCommunityId = communityId;
165
+ this._registeredAppId = appId;
166
+ } catch (e) {
167
+ console.warn('[Config] Failed to write workspace.json:', e.message);
168
+ }
169
+ }
170
+
171
+ cleanupWorkspaceRegistration() {
172
+ if (!this._workspacePath || !this._registeredCommunityId || !this._registeredAppId) return;
173
+ try {
174
+ const workspace = JSON.parse(fs.readFileSync(this._workspacePath, 'utf-8'));
175
+ const appEntry = workspace.communities?.[this._registeredCommunityId]?.apps?.[this._registeredAppId];
176
+ if (appEntry?.service) {
177
+ delete appEntry.service.port;
178
+ }
179
+ fs.writeFileSync(this._workspacePath, JSON.stringify(workspace, null, 2), 'utf-8');
180
+ console.log(`[Config] Cleaned up workspace registration for ${this._registeredCommunityId}/${this._registeredAppId}`);
181
+ } catch {
182
+ // Best-effort cleanup on shutdown
183
+ }
184
+ }
185
+
186
+ async initialize() {
187
+ // 1. Get Project ID
188
+ const auth = new GoogleAuth();
189
+ this.GOOGLE_PROJECT_ID = await auth.getProjectId();
190
+
191
+ // 2. Load from Secret Manager
192
+ if (this.CONFIG_SECRET_NAME) {
193
+ try {
194
+ const payload = await this.accessSecretVersion(
195
+ this.CONFIG_SECRET_NAME,
196
+ this.CONFIG_SECRET_VERSION
197
+ );
198
+ this._mergeConfig(JSON.parse(payload));
199
+ console.log(`[Config] Loaded from Secret Manager: ${this.CONFIG_SECRET_NAME}`);
200
+ } catch (error) {
201
+ console.warn(`[Config] Secret Manager unavailable:`, error.message);
202
+ }
203
+ }
204
+
205
+ // 3. Apply Dev Overrides
206
+ this._loadDevOverrides();
207
+
208
+ // 4. Validate Port (Allow 'auto' or 0)
209
+ if (!this.PORT && this.PORT !== 0) {
210
+ if (this.DEBUG_LOCAL && this.LOCAL_PORT) {
211
+ this.PORT = this.LOCAL_PORT;
212
+ } else {
213
+ // If dev, default to auto-assign
214
+ if (this.DEPLOY_ENV === 'dev' || this.DEBUG_LOCAL) {
215
+ this.PORT = 'auto';
216
+ } else {
217
+ throw new Error('[Config] CRITICAL: PORT not configured');
218
+ }
219
+ }
220
+ }
221
+
222
+ // 5. Default Core API URL if not set
223
+ if (!this.CORE_API_URL) {
224
+ this.CORE_API_URL = this.DEBUG_LOCAL
225
+ ? 'https://localhost:4000/apifront'
226
+ : 'https://descix-cloud.descix.net/apifront';
227
+ }
228
+
229
+ // 6. Register service port in workspace.json for descix-serve gateway
230
+ await this._registerWithWorkspace();
231
+
232
+ // Clean up registration on shutdown
233
+ process.on('SIGINT', () => this.cleanupWorkspaceRegistration());
234
+ process.on('SIGTERM', () => this.cleanupWorkspaceRegistration());
235
+
236
+ console.log(`[Config] Final: port=${this.PORT}, core=${this.CORE_API_URL}`);
237
+ }
238
+ }
239
+
240
+ const serviceConfig = new ServiceConfig();
241
+
242
+ async function initializeServiceConfig() {
243
+ await serviceConfig.initialize();
244
+ return serviceConfig;
245
+ }
246
+
247
+ export { initializeServiceConfig, serviceConfig as utils };
@@ -0,0 +1,18 @@
1
+ {
2
+ "serviceName": "sample-service",
3
+ "description": "DeSciX Add-on Service",
4
+ "configChecks": [
5
+ {
6
+ "key": "CONFIG_SECRET_NAME",
7
+ "source": ".env",
8
+ "required": true,
9
+ "description": "Secret Manager secret name"
10
+ },
11
+ {
12
+ "key": "CONFIG_SECRET_VERSION",
13
+ "source": ".env",
14
+ "required": true,
15
+ "description": "Secret Manager version"
16
+ }
17
+ ]
18
+ }