@descix/cli 2.0.2 → 2.0.4
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/README.md +0 -42
- package/agent-assets/guides/microservice-pattern.md +3 -1
- package/bin/descix.js +5 -28
- package/lib/agent-files.js +5 -2
- package/lib/app-scaffold.js +68 -0
- package/lib/commands/appInit.js +2 -0
- package/lib/commands/config.js +18 -4
- package/lib/commands/retired-verbs.js +11 -0
- package/lib/verb-invokes.generated.json +0 -3
- package/package.json +1 -1
- package/templates/agent-claude.md +6 -0
- package/templates/scaffolds/README.md +4 -0
- package/templates/scaffolds/frontpage/index.html +124 -0
- package/templates/scaffolds/microservice/package.json +1 -1
- package/templates/scaffolds/microservice/scripts/register.js +10 -0
- package/lib/commands/airdrop.js +0 -266
package/README.md
CHANGED
|
@@ -183,48 +183,6 @@ build variables the server returns injected into its environment:
|
|
|
183
183
|
}
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
## Airdrop Admin Operations (WS-ADMIN-B1)
|
|
187
|
-
|
|
188
|
-
Admin-only commands for triggering airdrop migration batch runs. Per CEO-D-MANUAL-TRIGGER-NO-CRON (2026-04-20), Cloud Scheduler cron was dropped from Round B in favor of operator-invoked manual triggers via this CLI.
|
|
189
|
-
|
|
190
|
-
**Access:** requires platform-admin membership (enforced server-side via `isPlatformAdmin(user)`).
|
|
191
|
-
|
|
192
|
-
### `descix airdrop execute-queue`
|
|
193
|
-
|
|
194
|
-
Manually trigger the server-side `airdrop_execute_queue` command on the target environment. Assembles `pending_migrations` rows into per-community batches and (when `BATCH_UPDATE_BALANCES_BROADCAST_ENABLED=true`) broadcasts to the Powch HD wallet. When the broadcast flag is off (default for DEV), batches are assembled but the downstream Powch handler returns `stopped_at_broadcast_boundary` — useful for verifying queue shape without spending MATIC.
|
|
195
|
-
|
|
196
|
-
**Read-only is the default.** Without `--apply`, the command runs as a dry run.
|
|
197
|
-
|
|
198
|
-
**Options:**
|
|
199
|
-
|
|
200
|
-
- `--dry-run`: Read-only preview — encode calldata, estimate gas, validate the net-zero invariant. No PK, no transaction, no state mutation. Mutually exclusive with `--apply`.
|
|
201
|
-
- `--apply`: Live execution. Requires `--signer-pk-file` or the interactive prompt, and requires `--community`.
|
|
202
|
-
- `--community <slug>`: Community slug for per-community batch scoping. **Required for `--apply`**; optional in dry-run mode, where omitting it returns an aggregate preview across communities.
|
|
203
|
-
- `--signer-pk-file <path>`: Path to a file containing the admin signer PK (`0x` + 64 hex). Used for `--apply`; when omitted, the PK is requested at an interactive no-echo prompt. Inline PK flags and environment variables are rejected on security grounds (process-listing leak).
|
|
204
|
-
- `--batch-size <n>`: Cap on users processed this run (server caps at `AIRDROP_MAX_RUN_USERS`; a larger `--batch-size` is clamped).
|
|
205
|
-
|
|
206
|
-
**Examples:**
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
# Dry run against DEV — safe to run repeatedly; no PK, no tx, no state mutation
|
|
210
|
-
descix airdrop execute-queue --env dev --dry-run
|
|
211
|
-
|
|
212
|
-
# Dry run scoped to one community
|
|
213
|
-
descix airdrop execute-queue --env dev --dry-run --community smile
|
|
214
|
-
|
|
215
|
-
# Live execution, capped at 10 users this invocation (prompts for the signer PK)
|
|
216
|
-
descix airdrop execute-queue --env dev --apply --community smile --batch-size 10
|
|
217
|
-
|
|
218
|
-
# Live execution reading the signer PK from a file
|
|
219
|
-
descix airdrop execute-queue --env dev --apply --community smile --signer-pk-file ./signer.key
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
**Testing:**
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
cd DeSciX_Core/descix-cli && npm test
|
|
226
|
-
```
|
|
227
|
-
|
|
228
186
|
## Documentation
|
|
229
187
|
|
|
230
188
|
For full documentation, please refer to the [DeSciX SDK Documentation](./SERVICE_README_sdk.md).
|
|
@@ -116,7 +116,9 @@ descix kb corpus sync -c <community> -a <app>
|
|
|
116
116
|
|
|
117
117
|
**What registration is, and is not.** It publishes your `manifest.json` — the command list —
|
|
118
118
|
and vectorizes the README you pass with `-r`, so `tell_me_how` can discover your tools.
|
|
119
|
-
`scripts/register.js` writes that manifest and logs the commands it published.
|
|
119
|
+
`scripts/register.js` writes that manifest and logs the commands it published. It targets a RUNNING
|
|
120
|
+
service: it first calls the service's own `/api` unsigned and registers only if the mesh guard
|
|
121
|
+
answers 401, so start the service (`npm run dev`), then run `npm run register`. There is no
|
|
120
122
|
separate developer-facing vectorize step — `descix microservice vectorize` exists but is an
|
|
121
123
|
admin-only utility (for re-vectorizing without re-registering), not part of the normal flow.
|
|
122
124
|
It is a DISCOVERY step.
|
package/bin/descix.js
CHANGED
|
@@ -28,7 +28,6 @@ import * as authCommands from '../lib/commands/auth.js';
|
|
|
28
28
|
import * as configCommands from '../lib/commands/config.js';
|
|
29
29
|
import * as buyCommands from '../lib/commands/buy.js';
|
|
30
30
|
import * as creditsCommands from '../lib/commands/credits.js';
|
|
31
|
-
import * as airdropCommands from '../lib/commands/airdrop.js';
|
|
32
31
|
import { MICROSERVICE_AUTH_MODEL } from '../lib/commands/microservice-auth-model.js';
|
|
33
32
|
import { runInit } from '../lib/commands/init.js';
|
|
34
33
|
// "May I prompt?" has ONE OWNER. No command in this file derives it.
|
|
@@ -430,32 +429,6 @@ creditsCommand
|
|
|
430
429
|
}
|
|
431
430
|
});
|
|
432
431
|
|
|
433
|
-
// ============ Airdrop Commands (WS-ADMIN-B1 manual-trigger) ============
|
|
434
|
-
// Per CEO-D-MANUAL-TRIGGER-NO-CRON (2026-04-20), airdrop batch execution is operator-triggered
|
|
435
|
-
// via this CLI command group rather than Cloud Scheduler cron. Server-side access is gated on
|
|
436
|
-
// platform-admin membership (`isPlatformAdmin(user)`); the server emits an on_chain_log row
|
|
437
|
-
// with `caller.operator_email` for audit.
|
|
438
|
-
|
|
439
|
-
const airdropCommand = program
|
|
440
|
-
.command('airdrop')
|
|
441
|
-
.description('Admin airdrop migration operations (WS-ADMIN-B1)');
|
|
442
|
-
|
|
443
|
-
airdropCommand
|
|
444
|
-
.command('execute-queue')
|
|
445
|
-
.description('Manually trigger airdrop_execute_queue on the target env (admin-only)')
|
|
446
|
-
.option('--community <slug>', 'Community slug for per-community batch scoping (REQUIRED for --apply)')
|
|
447
|
-
.option('--dry-run', 'Read-only preview: encode calldata, estimate gas, validate net-zero invariant. No PK, no tx, no state mutation.')
|
|
448
|
-
.option('--apply', 'Live execution: requires --signer-pk-file or interactive prompt-password. Mutually exclusive with --dry-run.')
|
|
449
|
-
.option('--signer-pk-file <path>', 'Path to file containing admin signer PK (0x + 64 hex). Required for --apply unless prompted interactively.')
|
|
450
|
-
.option('--batch-size <n>', 'Cap on users processed this run (server caps at AIRDROP_MAX_RUN_USERS)')
|
|
451
|
-
.action(async (options) => {
|
|
452
|
-
try {
|
|
453
|
-
await airdropCommands.executeQueue(options);
|
|
454
|
-
} catch (error) {
|
|
455
|
-
fail(error);
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
|
|
459
432
|
// ============ Sync Commands: REMOVED ============
|
|
460
433
|
// `sync assets`, `sync site` and `sync kb` are all gone, so the `sync` GROUP is gone with
|
|
461
434
|
// them: a registered verb with no working children is dead weight. Every retired verb is
|
|
@@ -2374,7 +2347,11 @@ siteCommand
|
|
|
2374
2347
|
|
|
2375
2348
|
console.log(chalk.cyan(`\n Site Upload: ${appId} → GCS\n`));
|
|
2376
2349
|
console.log(chalk.gray(` Source: ${sourceLabel}`));
|
|
2377
|
-
|
|
2350
|
+
// The write prefix is server-owned and FLAT ({env}/{appId}/site/ — community_id is
|
|
2351
|
+
// derived server-side from Products/{app_id} and a caller-supplied one is ignored).
|
|
2352
|
+
// Printing a locally-guessed community here once read as a changed write target and
|
|
2353
|
+
// held up a prod upload (FRAQTL wall, 2026-10-12) — say only what this client knows.
|
|
2354
|
+
console.log(chalk.gray(` Target: {env}/${appId}/site/ (flat; community resolved server-side)`));
|
|
2378
2355
|
if (options.preview) console.log(chalk.yellow(` Mode: PREVIEW`));
|
|
2379
2356
|
if (gitStatus.isGitRepo) {
|
|
2380
2357
|
console.log(chalk.gray(` Git: ${gitStatus.branch} @ ${gitStatus.shortHash}`));
|
package/lib/agent-files.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { readIdentity, isIdentityNamed } from './workspace-identity.js';
|
|
14
14
|
import { mcpServerEntry } from './invocation.js';
|
|
15
|
+
import { CLI_VERSION } from './cli-version.js';
|
|
15
16
|
|
|
16
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
18
|
const __dirname = path.dirname(__filename);
|
|
@@ -71,7 +72,7 @@ async function loadContext(workspaceRoot) {
|
|
|
71
72
|
// Friendly name: capitalize app_id or use as-is
|
|
72
73
|
const appName = appId.charAt(0).toUpperCase() + appId.slice(1);
|
|
73
74
|
|
|
74
|
-
return { appId, communityId, apiUrl: originLine, appName, originSource };
|
|
75
|
+
return { appId, communityId, apiUrl: originLine, appName, originSource, cliVersion: CLI_VERSION };
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/**
|
|
@@ -82,7 +83,9 @@ function render(template, vars) {
|
|
|
82
83
|
.replace(/\{\{appId\}\}/g, vars.appId)
|
|
83
84
|
.replace(/\{\{communityId\}\}/g, vars.communityId)
|
|
84
85
|
.replace(/\{\{apiUrl\}\}/g, vars.apiUrl)
|
|
85
|
-
.replace(/\{\{appName\}\}/g, vars.appName)
|
|
86
|
+
.replace(/\{\{appName\}\}/g, vars.appName)
|
|
87
|
+
.replace(/\{\{originSource\}\}/g, vars.originSource)
|
|
88
|
+
.replace(/\{\{cliVersion\}\}/g, vars.cliVersion);
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ONE OWNER of the files `descix app init` stamps into an app repo beyond the workspace and
|
|
3
|
+
* the agent instruction files (proposal 2026-10-09-standard-app-repo-form, items 3 and 5):
|
|
4
|
+
*
|
|
5
|
+
* .descix/manifests/site.json the deployable site set (`descix site upload` reads it)
|
|
6
|
+
* .claude-plugin/plugin.json the Claude plugin stub
|
|
7
|
+
* site/index.html the front-page template (templates/scaffolds/frontpage/)
|
|
8
|
+
*
|
|
9
|
+
* ONLY-IF-ABSENT: a file that exists is never read-modified, never overwritten, never reported as
|
|
10
|
+
* written. The agent files (CLAUDE.md …) stay with lib/agent-files.js; this module does not write
|
|
11
|
+
* them.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
const FRONTPAGE_TEMPLATE = path.join(
|
|
18
|
+
path.dirname(fileURLToPath(import.meta.url)), '..', 'templates', 'scaffolds', 'frontpage', 'index.html',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const escapeHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
22
|
+
|
|
23
|
+
async function writeIfAbsent(file, content) {
|
|
24
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
25
|
+
try {
|
|
26
|
+
await fs.writeFile(file, content, { encoding: 'utf8', flag: 'wx' });
|
|
27
|
+
return true;
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code === 'EEXIST') return false;
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} appPath the app repo root
|
|
36
|
+
* @param {{appId: string, communityId: string}} identity
|
|
37
|
+
* @returns {Promise<string[]>} repo-relative paths actually created
|
|
38
|
+
*/
|
|
39
|
+
export async function scaffoldAppRepo(appPath, { appId, communityId }) {
|
|
40
|
+
if (!appId || !communityId) {
|
|
41
|
+
throw new Error('scaffoldAppRepo: appId and communityId are required (no default would be true).');
|
|
42
|
+
}
|
|
43
|
+
const siteManifest = {
|
|
44
|
+
_purpose: `Deployable site set for the ${appId} app; sources are uploaded by descix site upload.`,
|
|
45
|
+
sources: [{ path: 'site' }],
|
|
46
|
+
buildCommand: null,
|
|
47
|
+
};
|
|
48
|
+
const plugin = {
|
|
49
|
+
name: appId,
|
|
50
|
+
description: `${appId}, an app of the ${communityId} community on DeSciX.`,
|
|
51
|
+
version: '0.1.0',
|
|
52
|
+
author: { name: communityId },
|
|
53
|
+
};
|
|
54
|
+
const frontpage = (await fs.readFile(FRONTPAGE_TEMPLATE, 'utf8'))
|
|
55
|
+
.replace(/\{\{APP_ID\}\}/g, escapeHtml(appId))
|
|
56
|
+
.replace(/\{\{COMMUNITY_ID\}\}/g, escapeHtml(communityId));
|
|
57
|
+
|
|
58
|
+
const files = [
|
|
59
|
+
['.descix/manifests/site.json', JSON.stringify(siteManifest, null, 2) + '\n'],
|
|
60
|
+
['.claude-plugin/plugin.json', JSON.stringify(plugin, null, 2) + '\n'],
|
|
61
|
+
['site/index.html', frontpage],
|
|
62
|
+
];
|
|
63
|
+
const created = [];
|
|
64
|
+
for (const [rel, content] of files) {
|
|
65
|
+
if (await writeIfAbsent(path.join(appPath, rel), content)) created.push(rel);
|
|
66
|
+
}
|
|
67
|
+
return created;
|
|
68
|
+
}
|
package/lib/commands/appInit.js
CHANGED
|
@@ -15,6 +15,7 @@ import path from 'path';
|
|
|
15
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
|
+
import { scaffoldAppRepo } from '../app-scaffold.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* @param {Object} apiClient an authenticated DeSciXApiClient
|
|
@@ -130,6 +131,7 @@ export async function runAppInit(apiClient, options) {
|
|
|
130
131
|
await fs.writeFile(descPath, `# ${appId}\n\nApplication description goes here.\n`);
|
|
131
132
|
}
|
|
132
133
|
console.log(chalk.gray(` Created: site/, microservice/, assets/`));
|
|
134
|
+
for (const f of await scaffoldAppRepo(appPath, { appId, communityId })) console.log(chalk.green(` ✓ ${f}`));
|
|
133
135
|
|
|
134
136
|
// 3. The default KB — CREATE leg only (see the adopt block above: map never creates).
|
|
135
137
|
let kbResult = null;
|
package/lib/commands/config.js
CHANGED
|
@@ -34,10 +34,24 @@ 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
|
-
// v3 single-app: the one app this workspace holds
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// v3 single-app: the one app this workspace holds. Every fact a verb can set is shown here,
|
|
38
|
+
// so the verb's effect is verifiable without hand-reading workspace.json (field, 2026-10-10:
|
|
39
|
+
// neither the adopted kbId nor env.gateway.port was printed by anything).
|
|
40
|
+
const app = workspaceConfig.env?.app;
|
|
41
|
+
if (app?.appId) {
|
|
42
|
+
console.log(chalk.white(` App: ${app.appId}`));
|
|
43
|
+
if (app.kbId) {
|
|
44
|
+
console.log(chalk.white(` KB: ${app.kbId}`));
|
|
45
|
+
}
|
|
46
|
+
if (app.site?.static) {
|
|
47
|
+
console.log(chalk.white(` Site: static ${app.site.static}`));
|
|
48
|
+
} else if (app.site?.port) {
|
|
49
|
+
console.log(chalk.white(` Site: dev server on port ${app.site.port} (${app.site.protocol || 'https'})`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const gatewayPort = workspaceConfig.env?.gateway?.port;
|
|
53
|
+
if (gatewayPort) {
|
|
54
|
+
console.log(chalk.white(` Gateway port: ${gatewayPort}`));
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
console.log('');
|
|
@@ -24,6 +24,11 @@ const kbSyncRefusal = (invocation) =>
|
|
|
24
24
|
`\`${CANONICAL_KB_CREATE}\`.`;
|
|
25
25
|
const KB_SYNC_DESCRIPTION = `REMOVED — use \`${CANONICAL_KB_SYNC}\``;
|
|
26
26
|
|
|
27
|
+
const AIRDROP_QUEUE_REFUSAL = (invocation) =>
|
|
28
|
+
`❌ \`${invocation}\` has been REMOVED: the pending_migrations queue it previewed (lane B) no longer ` +
|
|
29
|
+
`exists, and neither does the server command behind it.\n\n` +
|
|
30
|
+
`Distributions run through the offers rail on daita-sam (act plan + the role holder's own wallet signs).`;
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* Every retired verb. `invocation` is what a user types; `parent` is the id of the commander
|
|
29
34
|
* parent it hangs off (a root passed to registerAllRetiredVerbs, or an earlier row); `refusal`
|
|
@@ -52,6 +57,12 @@ export const RETIRED_VERBS = Object.freeze([
|
|
|
52
57
|
refusal: () =>
|
|
53
58
|
'"descix microservice register-delegate" has been removed: there is no per-app microservice ' +
|
|
54
59
|
'delegate key.\n' + MICROSERVICE_AUTH_MODEL },
|
|
60
|
+
// Lane B (the pending_migrations queue) is struck (CEO word Unkamon 1530eb6, daita-sam#4).
|
|
61
|
+
{ id: 'airdrop', parent: 'program', name: 'airdrop', invocation: 'descix airdrop',
|
|
62
|
+
description: 'removed — see error text', refusal: (invocation) => AIRDROP_QUEUE_REFUSAL(invocation) },
|
|
63
|
+
{ id: 'airdrop.execute-queue', parent: 'airdrop', name: 'execute-queue',
|
|
64
|
+
invocation: 'descix airdrop execute-queue', description: 'removed — see error text',
|
|
65
|
+
refusal: (invocation) => AIRDROP_QUEUE_REFUSAL(invocation) },
|
|
55
66
|
]);
|
|
56
67
|
|
|
57
68
|
/** Implementations deleted with the KB sync surfaces. No exported symbol survives without a caller. */
|
package/package.json
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## DeSciX lane
|
|
2
|
+
LANE: app ENV: {{apiUrl}} (origin source: {{originSource}}; this repo's own .descix/workspace.json env.apiUrl and wallet — `descix whoami` must agree, never an environment variable)
|
|
3
|
+
SDK: ONLY the published packages, as an external developer, invoked as `npx -y -p @descix/cli@{{cliVersion}} descix …` (an `npx` call without `-p @descix/cli` resolves the wrong package); never platform source
|
|
4
|
+
HANDBOOK: 1 `descix tell-me-how "<question>"` 2 the docs shipped inside the published packages (@descix/cli agent-assets/, @descix/app-sdk README.md) 3 the app-developer runbook, live at https://descix.net/prod/daita-docs/site/app-sdk/app-developer-runbook.html
|
|
5
|
+
PLATFORM WALL: when the handbook does not answer — stop, write the question with evidence (command, output, the CLI's origin banner line) and hand it to Sam to open a platform session. Never open platform source to answer it.
|
|
6
|
+
|
|
1
7
|
# DeSciX App Development
|
|
2
8
|
|
|
3
9
|
<!-- BOOTSTRAP MODE: This file was auto-generated by DeSciX.
|
|
@@ -39,6 +39,10 @@ A complete Express microservice with:
|
|
|
39
39
|
descix microservice init
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
### Front page (`frontpage/`)
|
|
43
|
+
|
|
44
|
+
The shared front-page template (`index.html`: head styles, `<nav class="nav">`, `<header class="hero" id="top">`, `<section class="sec" id="…">` blocks, `<footer>`). It is stamped by `descix app init` as `site/index.html`, ONLY if that file is absent, with `{{APP_ID}}` / `{{COMMUNITY_ID}}` filled by `lib/app-scaffold.js`. It is not a `site init` scaffold type. Each repo owns its own divergence after the stamp; no repo reads this template across repositories.
|
|
45
|
+
|
|
42
46
|
## Usage
|
|
43
47
|
|
|
44
48
|
Run scaffold commands from your app directory (auto-detects context):
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<title>{{APP_ID}} — a {{COMMUNITY_ID}} app on DeSciX</title>
|
|
7
|
+
<meta name="description" content="{{APP_ID}}, an app of the {{COMMUNITY_ID}} community on DeSciX. Replace this description with what the app is for.">
|
|
8
|
+
<style>
|
|
9
|
+
:root{
|
|
10
|
+
--bg:#0b0f14; --bg2:#10161e; --panel:#151d27; --line:#233040; --line2:#2f3f52;
|
|
11
|
+
--ink:#e9eef3; --ink2:#b3bfcc; --mute:#7f8ea0;
|
|
12
|
+
--cyan:#5fcbe6; --cyan2:#3aa9c6; --cyan-ink:#06202a;
|
|
13
|
+
--serif:Georgia,"Times New Roman",serif;
|
|
14
|
+
--sans:"Helvetica Neue",Arial,sans-serif;
|
|
15
|
+
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
16
|
+
color-scheme:dark;
|
|
17
|
+
}
|
|
18
|
+
*{box-sizing:border-box}
|
|
19
|
+
[hidden]{display:none!important}
|
|
20
|
+
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:17px;line-height:1.55;-webkit-font-smoothing:antialiased}
|
|
21
|
+
a{color:var(--cyan);text-decoration:none}
|
|
22
|
+
a:hover{text-decoration:underline}
|
|
23
|
+
a:focus-visible,button:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
|
|
24
|
+
img{max-width:100%;display:block}
|
|
25
|
+
h1,h2,h3{font-family:var(--serif);font-weight:500;line-height:1.08;margin:0;text-wrap:balance;letter-spacing:-.01em}
|
|
26
|
+
h2{font-size:clamp(30px,4.2vw,46px)}
|
|
27
|
+
h3{font-size:22px}
|
|
28
|
+
p{margin:0}
|
|
29
|
+
.wrap{max-width:1120px;margin:0 auto;padding-inline:20px}
|
|
30
|
+
.eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--cyan)}
|
|
31
|
+
.sec{padding-block:88px;border-top:1px solid var(--line)}
|
|
32
|
+
.sec-head{display:grid;gap:14px;max-width:760px;margin-bottom:44px}
|
|
33
|
+
.lede{font-size:19px;color:var(--ink2);max-width:62ch}
|
|
34
|
+
.btn{display:inline-flex;align-items:center;gap:10px;padding:14px 22px;border-radius:4px;font-family:var(--sans);font-weight:600;font-size:16px;border:1px solid transparent;cursor:pointer;text-decoration:none!important}
|
|
35
|
+
.btn-primary{background:var(--cyan);color:var(--cyan-ink)}
|
|
36
|
+
.btn-primary:hover{background:#7ad6ec}
|
|
37
|
+
.btn-ghost{background:transparent;color:var(--ink);border-color:var(--line2)}
|
|
38
|
+
.btn-ghost:hover{border-color:var(--cyan);color:var(--cyan)}
|
|
39
|
+
code{font-family:var(--mono);font-size:.92em;color:var(--cyan);background:rgba(95,203,230,.08);padding:2px 6px;border-radius:3px}
|
|
40
|
+
|
|
41
|
+
/* nav */
|
|
42
|
+
.nav{position:sticky;top:env(safe-area-inset-top,0px);z-index:20;background:rgba(11,15,20,.86);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
|
|
43
|
+
.nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:20px;height:64px}
|
|
44
|
+
.brand{display:flex;align-items:baseline;gap:10px;color:var(--ink);text-decoration:none!important}
|
|
45
|
+
.brand b{font-family:var(--serif);font-weight:600;font-size:20px;letter-spacing:.01em}
|
|
46
|
+
.brand span{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--mute)}
|
|
47
|
+
.navlinks{display:flex;gap:26px;font-size:15px}
|
|
48
|
+
.navlinks a{color:var(--ink2)}
|
|
49
|
+
.navlinks a:hover{color:var(--cyan);text-decoration:none}
|
|
50
|
+
@media (max-width:820px){.navlinks{display:none}}
|
|
51
|
+
|
|
52
|
+
/* hero */
|
|
53
|
+
.hero{position:relative;overflow:hidden;border-bottom:1px solid var(--line)}
|
|
54
|
+
.hero .wrap{position:relative;padding-block:96px 84px;display:grid;gap:26px}
|
|
55
|
+
.hero h1{font-size:clamp(40px,6.4vw,78px);font-weight:300;max-width:12ch}
|
|
56
|
+
.hero h1 em{font-style:italic;font-weight:500;color:var(--cyan)}
|
|
57
|
+
.hero .lede{font-size:20px;max-width:56ch}
|
|
58
|
+
.cta-row{display:flex;flex-wrap:wrap;gap:12px;margin-top:6px}
|
|
59
|
+
|
|
60
|
+
/* skeleton cards */
|
|
61
|
+
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
|
|
62
|
+
.card{border:1px solid var(--line);border-radius:6px;background:var(--bg2);padding:24px 22px;display:grid;gap:10px;align-content:start}
|
|
63
|
+
.card p{font-size:15px;color:var(--ink2)}
|
|
64
|
+
@media (max-width:820px){.cards{grid-template-columns:1fr}}
|
|
65
|
+
|
|
66
|
+
/* footer */
|
|
67
|
+
footer{border-top:1px solid var(--line);padding-block:44px 60px;color:var(--mute);font-size:14px}
|
|
68
|
+
footer .wrap{display:grid;gap:22px}
|
|
69
|
+
footer .row{display:flex;flex-wrap:wrap;gap:8px 24px}
|
|
70
|
+
footer .row a{color:var(--ink2)}
|
|
71
|
+
footer .legal{max-width:80ch;font-size:13px}
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
|
|
76
|
+
<nav class="nav"><div class="wrap">
|
|
77
|
+
<a class="brand" href="#top"><b>{{APP_ID}}</b><span>{{COMMUNITY_ID}}</span></a>
|
|
78
|
+
<div class="navlinks">
|
|
79
|
+
<a href="#about">About</a><a href="#next">Next steps</a>
|
|
80
|
+
</div>
|
|
81
|
+
</div></nav>
|
|
82
|
+
|
|
83
|
+
<header class="hero" id="top">
|
|
84
|
+
<div class="wrap">
|
|
85
|
+
<p class="eyebrow">A {{COMMUNITY_ID}} app on DeSciX</p>
|
|
86
|
+
<h1>{{APP_ID}}, <em>in one sentence.</em></h1>
|
|
87
|
+
<p class="lede">This is the scaffold front page <code>descix app init</code> stamped for {{APP_ID}}. It is yours: replace this paragraph with what the app does and who it is for.</p>
|
|
88
|
+
<div class="cta-row">
|
|
89
|
+
<a class="btn btn-primary" href="#about">Learn more</a>
|
|
90
|
+
<a class="btn btn-ghost" href="#next">Make this page yours</a>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</header>
|
|
94
|
+
|
|
95
|
+
<section class="sec" id="about"><div class="wrap">
|
|
96
|
+
<div class="sec-head">
|
|
97
|
+
<p class="eyebrow">About</p>
|
|
98
|
+
<h2>What {{APP_ID}} is for.</h2>
|
|
99
|
+
</div>
|
|
100
|
+
<p class="lede">Placeholder copy from the scaffold. Say what {{APP_ID}} offers to the {{COMMUNITY_ID}} community, in the app's own words.</p>
|
|
101
|
+
</div></section>
|
|
102
|
+
|
|
103
|
+
<section class="sec" id="next"><div class="wrap">
|
|
104
|
+
<div class="sec-head">
|
|
105
|
+
<p class="eyebrow">Next steps</p>
|
|
106
|
+
<h2>Make this page yours.</h2>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="cards">
|
|
109
|
+
<div class="card"><h3>Edit it</h3><p>This file is <code>site/index.html</code> in your repository. Nothing regenerates it; your edits are kept.</p></div>
|
|
110
|
+
<div class="card"><h3>Upload it</h3><p>Deploy the site set with <code>descix site upload</code>.</p></div>
|
|
111
|
+
<div class="card"><h3>Ask the handbook</h3><p>Run <code>descix tell-me-how</code> for every capability the CLI offers.</p></div>
|
|
112
|
+
</div>
|
|
113
|
+
</div></section>
|
|
114
|
+
|
|
115
|
+
<footer><div class="wrap">
|
|
116
|
+
<div class="brand"><b>{{APP_ID}}</b><span>{{COMMUNITY_ID}}</span></div>
|
|
117
|
+
<div class="row">
|
|
118
|
+
<a href="#about">About</a><a href="#next">Next steps</a>
|
|
119
|
+
</div>
|
|
120
|
+
<p class="legal">{{APP_ID}} is an app of the {{COMMUNITY_ID}} community on DeSciX. Replace this line with your own legal text.</p>
|
|
121
|
+
</div></footer>
|
|
122
|
+
|
|
123
|
+
</body>
|
|
124
|
+
</html>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"gen-jwt-secret": "node -e 'console.log(require(\"crypto\").randomBytes(32).toString(\"hex\"))'"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@descix/cloud-core": "^
|
|
14
|
+
"@descix/cloud-core": "^2.0.0",
|
|
15
15
|
"@descix/platform-api": "^2.0.0",
|
|
16
16
|
"@google-cloud/secret-manager": "^5.0.1",
|
|
17
17
|
"@google-cloud/storage": "^7.7.0",
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* (audience = CORE_API_URL). On Cloud Run that works with no setup. Under local developer ADC,
|
|
24
24
|
* ID-token minting is unsupported without impersonation and this reports a named failure — it
|
|
25
25
|
* does not fall back to anything.
|
|
26
|
+
*
|
|
27
|
+
* REGISTRATION TARGETS A RUNNING SERVICE. Before it registers, the bootstrap calls this service's
|
|
28
|
+
* own /api UNSIGNED and registers only if the mesh guard answers 401 (the mesh startup self-test).
|
|
29
|
+
* The origin it calls is derived from utils.PORT — the same config value app.js binds — so start
|
|
30
|
+
* the service (npm run dev) first, in the same checkout, then run this script.
|
|
26
31
|
*/
|
|
27
32
|
|
|
28
33
|
import fs from 'fs';
|
|
@@ -51,12 +56,17 @@ async function register() {
|
|
|
51
56
|
// Used ONLY to name the derived domain in a message. The platform, not this script,
|
|
52
57
|
// owns the derivation.
|
|
53
58
|
siteDomain: utils.SITE_DOMAIN,
|
|
59
|
+
// The running service's own listener, from the config owner app.js binds (utils.PORT).
|
|
60
|
+
meshSelfUrl: `http://127.0.0.1:${utils.PORT}`,
|
|
54
61
|
});
|
|
55
62
|
|
|
56
63
|
const result = await bootstrap.register();
|
|
57
64
|
|
|
58
65
|
if (result.status !== 'ok') {
|
|
59
66
|
console.error(`[Register] ${manifest.service.name}: registration ${result.status} — ${result.error || result.reason}`);
|
|
67
|
+
if (String(result.error || result.reason).includes('mesh self-test FAILED')) {
|
|
68
|
+
console.error('[Register] REMEDY: start the service (npm run dev), then re-run npm run register');
|
|
69
|
+
}
|
|
60
70
|
process.exit(1);
|
|
61
71
|
}
|
|
62
72
|
console.log(`[Register] SUCCESS: ${manifest.service.name} registered via register_service`);
|
package/lib/commands/airdrop.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Airdrop Commands
|
|
3
|
-
*
|
|
4
|
-
* Admin-facing CLI commands for WS-ADMIN-B1 airdrop migration operations.
|
|
5
|
-
* Per CEO-D-MANUAL-TRIGGER-NO-CRON (2026-04-20), Cloud Scheduler cron was dropped
|
|
6
|
-
* from Round B in favor of operator-invoked manual triggers via this CLI.
|
|
7
|
-
*
|
|
8
|
-
* Per CEO-D-B1-FIX-DESIGN-LOCK (2026-04-28), the CLI signature is:
|
|
9
|
-
* descix airdrop execute-queue --community <slug> [--dry-run | --signer-pk-file <path> --apply]
|
|
10
|
-
*
|
|
11
|
-
* --community <slug> REQUIRED for --apply mode (per-community batch scoping). Optional
|
|
12
|
-
* in --dry-run mode (returns aggregate-across-communities preview).
|
|
13
|
-
* --dry-run Read-only preview path. No PK prompt. No tx. No state mutation.
|
|
14
|
-
* --apply Live execution path. Requires --signer-pk-file or interactive
|
|
15
|
-
* prompt-password. Mutually exclusive with --dry-run.
|
|
16
|
-
* --signer-pk-file <path> File containing the admin's per-invocation signer PK (0x + 64 hex,
|
|
17
|
-
* one line). Inline --signer-pk and env vars REJECTED on security
|
|
18
|
-
* grounds (process listing leak).
|
|
19
|
-
*
|
|
20
|
-
* Access: requires admin session (platform-admin group membership enforced server-side
|
|
21
|
-
* in `airdrop_execute_queue` via `isPlatformAdmin(user)`). CLI-side, we require an
|
|
22
|
-
* authenticated session (`requireAuth`). The server rejects non-admin users.
|
|
23
|
-
*
|
|
24
|
-
* Safety: the `BATCH_UPDATE_BALANCES_BROADCAST_ENABLED` flag gates the actual on-chain
|
|
25
|
-
* broadcast in Powch. While that flag is false, `airdrop_execute_queue --apply` will assemble
|
|
26
|
-
* batches but Powch returns `stopped_at_broadcast_boundary` — useful for verifying queue
|
|
27
|
-
* assembly without spending MATIC.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import fs from 'fs';
|
|
31
|
-
// "May I prompt?" has ONE OWNER. This site holds no TTY check of its own.
|
|
32
|
-
import { requireInteractive } from '../interactive.js';
|
|
33
|
-
import readline from 'readline';
|
|
34
|
-
import chalk from 'chalk';
|
|
35
|
-
import { DeSciXApiClient } from '../api-client.js';
|
|
36
|
-
import { requireAuth } from '../auth-guard.js';
|
|
37
|
-
|
|
38
|
-
const PK_REGEX = /^0x[a-fA-F0-9]{64}$/;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Read a signer PK from a file. Validates 0x + 64-hex format. Trims trailing whitespace
|
|
42
|
-
* and a trailing newline only — does NOT mutate any other formatting.
|
|
43
|
-
*
|
|
44
|
-
* @param {string} path - Filesystem path to the PK file.
|
|
45
|
-
* @returns {string} Validated PK in `0x` + 64 hex form.
|
|
46
|
-
* @throws {Error} If the file is missing or contents are malformed.
|
|
47
|
-
*/
|
|
48
|
-
export function readSignerPkFromFile(path) {
|
|
49
|
-
let raw;
|
|
50
|
-
try {
|
|
51
|
-
raw = fs.readFileSync(path, 'utf8');
|
|
52
|
-
} catch (e) {
|
|
53
|
-
throw new Error(`--signer-pk-file: cannot read ${path}: ${e.message}`);
|
|
54
|
-
}
|
|
55
|
-
const trimmed = raw.trim();
|
|
56
|
-
if (!PK_REGEX.test(trimmed)) {
|
|
57
|
-
// CRITICAL: do not echo the contents (even partial) into the error message.
|
|
58
|
-
throw new Error(`--signer-pk-file: contents do not match expected 0x + 64 hex format`);
|
|
59
|
-
}
|
|
60
|
-
return trimmed;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Interactive password prompt (no echo). Reads a line of stdin with terminal echo disabled
|
|
65
|
-
* so the typed PK never appears on screen. Validates 0x + 64-hex format.
|
|
66
|
-
*
|
|
67
|
-
* @returns {Promise<string>} Validated PK in `0x` + 64 hex form.
|
|
68
|
-
*/
|
|
69
|
-
export async function promptSignerPkInteractive({ stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
70
|
-
// Gate FIRST: a non-TTY here previously hung forever holding a secret prompt open. This site
|
|
71
|
-
// keeps its own readline because it must disable terminal echo so the PK is never displayed;
|
|
72
|
-
// the DECISION of whether prompting is allowed is still the owner's alone.
|
|
73
|
-
requireInteractive({
|
|
74
|
-
what: 'descix airdrop',
|
|
75
|
-
question: 'signer private key',
|
|
76
|
-
nonInteractiveForm: ['descix airdrop --signer-pk-file <path> # reads the key from a file, never echoed']
|
|
77
|
-
});
|
|
78
|
-
return await new Promise((resolve, reject) => {
|
|
79
|
-
const rl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
|
|
80
|
-
// Disable echo so the PK isn't visible while typing.
|
|
81
|
-
const originalWrite = stdout.write.bind(stdout);
|
|
82
|
-
stdout.write = (str, ...rest) => {
|
|
83
|
-
// Suppress only the echoed characters during the prompt; allow our prompt to render.
|
|
84
|
-
if (typeof str === 'string' && str.length === 1) return true;
|
|
85
|
-
return originalWrite(str, ...rest);
|
|
86
|
-
};
|
|
87
|
-
stdout.write('Enter signer PK (0x + 64 hex, will not be echoed): ');
|
|
88
|
-
rl.question('', (answer) => {
|
|
89
|
-
stdout.write = originalWrite;
|
|
90
|
-
stdout.write('\n');
|
|
91
|
-
rl.close();
|
|
92
|
-
const trimmed = (answer || '').trim();
|
|
93
|
-
if (!PK_REGEX.test(trimmed)) {
|
|
94
|
-
reject(new Error('prompt-password: input does not match expected 0x + 64 hex format'));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
resolve(trimmed);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Execute the airdrop migration batch queue on the target environment.
|
|
104
|
-
*
|
|
105
|
-
* @param {Object} options
|
|
106
|
-
* @param {string} [options.community] - Community slug for per-community batch scoping.
|
|
107
|
-
* REQUIRED for --apply mode; optional for --dry-run mode (omitted = aggregate preview).
|
|
108
|
-
* @param {boolean} [options.dryRun] - If true, read-only preview path. Mutually exclusive with --apply.
|
|
109
|
-
* @param {boolean} [options.apply] - If true, live execution path. Requires --signer-pk-file or
|
|
110
|
-
* interactive prompt-password. Mutually exclusive with --dry-run.
|
|
111
|
-
* @param {string} [options.signerPkFile] - Path to file containing the admin's signer PK.
|
|
112
|
-
* @param {number} [options.batchSize] - Optional cap on number of users processed this run.
|
|
113
|
-
* @param {Object} [options.apiClient] - Optional injected API client (for tests).
|
|
114
|
-
* @param {Function} [options.promptFn] - Optional injected interactive PK prompt (for tests).
|
|
115
|
-
*/
|
|
116
|
-
export async function executeQueue(options) {
|
|
117
|
-
const apiClient = options.apiClient || new DeSciXApiClient();
|
|
118
|
-
await requireAuth(apiClient);
|
|
119
|
-
|
|
120
|
-
// Mode resolution: --dry-run and --apply are mutually exclusive. Default is dry-run when
|
|
121
|
-
// no --apply is provided (read-only is the safer default per CEO-D-B1-FIX-DESIGN-LOCK §4.A).
|
|
122
|
-
if (options.dryRun && options.apply) {
|
|
123
|
-
throw new Error('--dry-run and --apply are mutually exclusive (CEO-D-B1-FIX-DESIGN-LOCK Decision 1).');
|
|
124
|
-
}
|
|
125
|
-
const isDryRun = options.dryRun === true || (!options.apply && options.dryRun !== false);
|
|
126
|
-
const isApply = options.apply === true;
|
|
127
|
-
|
|
128
|
-
if (isApply && !options.community) {
|
|
129
|
-
throw new Error('--community <slug> is required for --apply mode (per-community batch scoping per CEO-D-B1-FIX-DESIGN-LOCK Decision 3).');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Resolve signer PK for --apply mode: --signer-pk-file (default) or interactive prompt fallback.
|
|
133
|
-
let signerPkHex = null;
|
|
134
|
-
if (isApply) {
|
|
135
|
-
if (options.signerPkFile) {
|
|
136
|
-
signerPkHex = readSignerPkFromFile(options.signerPkFile);
|
|
137
|
-
} else {
|
|
138
|
-
const promptFn = options.promptFn || promptSignerPkInteractive;
|
|
139
|
-
signerPkHex = await promptFn();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const params = {};
|
|
144
|
-
if (options.community) params.community = options.community;
|
|
145
|
-
params.mode = isApply ? 'apply' : 'dry-run';
|
|
146
|
-
if (signerPkHex) params.signer_pk_hex = signerPkHex;
|
|
147
|
-
if (options.batchSize !== undefined && options.batchSize !== null) {
|
|
148
|
-
const bs = Number(options.batchSize);
|
|
149
|
-
if (!Number.isFinite(bs) || bs <= 0) {
|
|
150
|
-
throw new Error(`--batch-size must be a positive integer, got: ${options.batchSize}`);
|
|
151
|
-
}
|
|
152
|
-
params.batch_size = bs;
|
|
153
|
-
}
|
|
154
|
-
// Legacy boolean dry_run parameter (preserved for backwards-compat with existing scripts).
|
|
155
|
-
if (isDryRun && !options.community) {
|
|
156
|
-
params.dry_run = true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const envLabel = process.env.DESCIX_API_URL || '(workspace default)';
|
|
160
|
-
const mode = isApply ? chalk.green('APPLY') : chalk.yellow('DRY-RUN');
|
|
161
|
-
console.log(chalk.cyan(`\nairdrop execute-queue [${mode}] target=${envLabel}`));
|
|
162
|
-
if (options.community) {
|
|
163
|
-
console.log(chalk.gray(` community: ${options.community}`));
|
|
164
|
-
}
|
|
165
|
-
if (params.batch_size) {
|
|
166
|
-
console.log(chalk.gray(` batch_size: ${params.batch_size}`));
|
|
167
|
-
}
|
|
168
|
-
if (signerPkHex) {
|
|
169
|
-
// Just confirm a PK is loaded; never echo any portion of it.
|
|
170
|
-
console.log(chalk.gray(` signer_pk: loaded (0x${signerPkHex.slice(2,6)}…${signerPkHex.slice(-4)})`));
|
|
171
|
-
}
|
|
172
|
-
console.log('');
|
|
173
|
-
|
|
174
|
-
let response;
|
|
175
|
-
try {
|
|
176
|
-
response = await apiClient.invoke('airdrop_execute_queue', params);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
// Auth/permission errors surface as HTTP-level failures with readable messages.
|
|
179
|
-
// CRITICAL: ensure no echo of params.signer_pk_hex into any error message we print.
|
|
180
|
-
const safeMsg = String(error.message || '').replace(/0x[a-fA-F0-9]{64}/g, '0x[REDACTED]');
|
|
181
|
-
console.error(chalk.red(`\nairdrop_execute_queue failed: ${safeMsg}\n`));
|
|
182
|
-
if (/platform-admin|service-account/i.test(error.message)) {
|
|
183
|
-
console.error(chalk.gray(' This command requires a platform-admin account.'));
|
|
184
|
-
console.error(chalk.gray(' Verify your membership in the DESCIX_ADMIN_GROUP Google Group.\n'));
|
|
185
|
-
}
|
|
186
|
-
throw error;
|
|
187
|
-
} finally {
|
|
188
|
-
// Belt-and-suspenders: clear local PK var after the single use. We deliberately
|
|
189
|
-
// do NOT mutate `params.signer_pk_hex` here because the api-client/axios layer may
|
|
190
|
-
// still be serializing the body, and modern axios keeps no reference after the
|
|
191
|
-
// request resolves anyway. The local var nulling is the meaningful action.
|
|
192
|
-
signerPkHex = null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const result = response?.message || response;
|
|
196
|
-
if (!result || typeof result !== 'object') {
|
|
197
|
-
throw new Error('airdrop_execute_queue returned no result');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (isDryRun && result.mode === 'dry-run') {
|
|
201
|
-
// Render the §4.F dry-run summary table.
|
|
202
|
-
console.log(chalk.bold('Dry-Run Summary:'));
|
|
203
|
-
console.log(` community: ${chalk.cyan(result.community || '(all)')}`);
|
|
204
|
-
if (result.token_symbol) console.log(` token_symbol: ${result.token_symbol}`);
|
|
205
|
-
if (result.token_contract_address) console.log(` token_contract: ${result.token_contract_address}`);
|
|
206
|
-
if (result.abi_source) console.log(` abi_source: ${result.abi_source.doc_id} (v${result.abi_source.version || '?'})`);
|
|
207
|
-
console.log(` transfers_total: ${chalk.yellow(result.transfers_total)}`);
|
|
208
|
-
console.log(` batches_total: ${result.batches_total}`);
|
|
209
|
-
console.log(` debit_total: ${result.debit_total}`);
|
|
210
|
-
console.log(` credit_total: ${result.credit_total}`);
|
|
211
|
-
const nz = result.net_zero_assertion || {};
|
|
212
|
-
const nzLabel = nz.passes ? chalk.green('PASS') : chalk.red('FAIL');
|
|
213
|
-
console.log(` net_zero_assertion: ${nzLabel} (Σ=${nz.sum})`);
|
|
214
|
-
const pbc = result.pre_batch_checks || {};
|
|
215
|
-
console.log(` pre_batch_checks: ${pbc.passes ? chalk.green('PASS') : chalk.red('FAIL — --apply would refuse')}`);
|
|
216
|
-
for (const r of (pbc.refusals || [])) {
|
|
217
|
-
console.log(chalk.red(` ${r.kind} ${r.wallet} (${r.community_id}) holds ${r.held}, needs ${r.required} base units — rows ${(r.pending_ids || []).join(', ')}`));
|
|
218
|
-
}
|
|
219
|
-
console.log(` unique_source_wallets: ${result.unique_source_wallets}`);
|
|
220
|
-
console.log(` unique_master_wallets: ${result.unique_master_wallets}`);
|
|
221
|
-
console.log(` prospective_batch_id: ${result.prospective_batch_id}`);
|
|
222
|
-
if (result.gas_estimate) {
|
|
223
|
-
console.log(` gas_estimate: ${result.gas_estimate.units || '?'} units @ ${result.gas_estimate.gas_price_gwei || '?'} gwei`);
|
|
224
|
-
if (result.gas_estimate.matic_cost_estimate) {
|
|
225
|
-
console.log(` estimated_cost: ~${result.gas_estimate.matic_cost_estimate} MATIC`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
console.log('');
|
|
229
|
-
console.log(chalk.gray('No PK supplied. No tx submitted. No DB rows mutated.'));
|
|
230
|
-
console.log(chalk.gray('Re-run with --apply --signer-pk-file <path> to commit.'));
|
|
231
|
-
console.log('');
|
|
232
|
-
return result;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
console.log(chalk.bold('Result:'));
|
|
236
|
-
console.log(` batches_executed: ${chalk.yellow(result.batches_executed ?? 0)}`);
|
|
237
|
-
console.log(` total_users: ${chalk.yellow(result.total_users ?? 0)}`);
|
|
238
|
-
console.log(` dry_run: ${result.dry_run ? chalk.yellow('true') : chalk.gray('false')}`);
|
|
239
|
-
|
|
240
|
-
const perCommunity = result.per_community || {};
|
|
241
|
-
const communityKeys = Object.keys(perCommunity);
|
|
242
|
-
if (communityKeys.length) {
|
|
243
|
-
console.log(` per_community:`);
|
|
244
|
-
for (const cid of communityKeys) {
|
|
245
|
-
console.log(` ${cid}: ${perCommunity[cid]}`);
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
console.log(` per_community: ${chalk.gray('{}')}`);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (result.caller) {
|
|
252
|
-
const who = result.caller.operator_email || result.caller.service_account || '(unknown)';
|
|
253
|
-
console.log(chalk.gray(` caller: ${who}`));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
console.log('');
|
|
257
|
-
if (isApply) {
|
|
258
|
-
console.log(chalk.gray('Apply complete. See on_chain_log for the trigger audit row.'));
|
|
259
|
-
} else {
|
|
260
|
-
console.log(chalk.gray('Dry-run complete. No broadcast attempted. No on_chain_log broadcast row written.'));
|
|
261
|
-
}
|
|
262
|
-
console.log('');
|
|
263
|
-
return result;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export default { executeQueue, readSignerPkFromFile, promptSignerPkInteractive };
|