@formigio/fazemos-cli 0.10.17 → 0.10.20
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/dist/dispatch.d.ts +108 -6
- package/dist/dispatch.js +135 -8
- package/dist/dispatch.js.map +1 -1
- package/dist/index.js +296 -35
- package/dist/index.js.map +1 -1
- package/dist/wait-for-pipeline.d.ts +120 -0
- package/dist/wait-for-pipeline.js +337 -0
- package/dist/wait-for-pipeline.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,8 +10,9 @@ import { isProjectConnectionUnavailable, renderProjectConnectionUnavailableCopy,
|
|
|
10
10
|
import { loadYaml, summarize } from './yaml/load.js';
|
|
11
11
|
import { printFindings, printJson } from './yaml/format.js';
|
|
12
12
|
import { validateManifest } from './manifest/checks.js';
|
|
13
|
-
import { findLocalRegistry, resolveRole, buildInboxFile, writeInboxFile,
|
|
13
|
+
import { findLocalRegistry, resolveRole, buildInboxFile, writeInboxFile, writeInboxFileAtPath, buildRolesSyncPayload, findUnprocessedInboxFiles, computeFileHash, gitCommitInboxFile, } from './dispatch.js';
|
|
14
14
|
import { execSync } from 'child_process';
|
|
15
|
+
import { parseExecutionsJson, resolveWaitOptions, waitForPipelines, buildAwsCommand, validateExecutionEntry, } from './wait-for-pipeline.js';
|
|
15
16
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
16
17
|
import { fileURLToPath } from 'url';
|
|
17
18
|
import { dirname, resolve, basename } from 'path';
|
|
@@ -4475,6 +4476,70 @@ pipelines
|
|
|
4475
4476
|
process.exit(1);
|
|
4476
4477
|
}
|
|
4477
4478
|
});
|
|
4479
|
+
// ── P8/I55 — deploy-completion gate ──
|
|
4480
|
+
// Poll one or more CodePipeline executions until each reaches a terminal state.
|
|
4481
|
+
// Shells out to the `aws` CLI via execSync (no @aws-sdk dependency, consistent
|
|
4482
|
+
// with the existing CLI pattern); credentials arrive via the ECS task-role
|
|
4483
|
+
// metadata service inside the agent container. Exit-code contract (spec §3.3):
|
|
4484
|
+
// 0 Succeeded (or empty map) · 2 Failed/Stopped · 3 Superseded · 4 timeout · 5 aws-call-failure.
|
|
4485
|
+
pipelines
|
|
4486
|
+
.command('wait-for-pipeline')
|
|
4487
|
+
.description('Poll CodePipeline execution(s) until terminal; exit 0 on Succeeded, non-zero with a structured diagnostic otherwise (deploy-completion gate)')
|
|
4488
|
+
.argument('[execution-id]', 'CodePipeline pipeline-execution-id (single-pair form; requires --pipeline). Omit when using --executions-json.')
|
|
4489
|
+
.option('--pipeline <name>', 'CodePipeline name (required with the single-pair form; execution-id is only unique within a pipeline)')
|
|
4490
|
+
.option('--executions-json <json>', 'Bulk form: JSON map of {pipelineName: executionId}. Empty/{} is a documented no-op (exit 0).')
|
|
4491
|
+
.option('--timeout <duration>', 'Total wall-clock budget (15m, 900s, or 900). Default 15m.')
|
|
4492
|
+
.option('--poll-interval <duration>', 'Per-poll sleep (floor 60s; values below are clamped + warned). Default 60s.')
|
|
4493
|
+
.option('--region <region>', 'AWS region (reads AWS_REGION/AWS_DEFAULT_REGION, else us-west-2). Passed explicitly to every aws call.')
|
|
4494
|
+
.action(async (executionId, opts) => {
|
|
4495
|
+
try {
|
|
4496
|
+
// Build the (pipeline, executionId) list from whichever form was used.
|
|
4497
|
+
let executions;
|
|
4498
|
+
if (opts.executionsJson !== undefined) {
|
|
4499
|
+
if (executionId || opts.pipeline) {
|
|
4500
|
+
console.error(chalk.red('wait-for-pipeline: pass EITHER --executions-json OR an execution-id + --pipeline, not both'));
|
|
4501
|
+
process.exit(1);
|
|
4502
|
+
}
|
|
4503
|
+
executions = parseExecutionsJson(opts.executionsJson);
|
|
4504
|
+
}
|
|
4505
|
+
else {
|
|
4506
|
+
if (!executionId) {
|
|
4507
|
+
console.error(chalk.red('wait-for-pipeline: an execution-id (with --pipeline) or --executions-json is required'));
|
|
4508
|
+
process.exit(1);
|
|
4509
|
+
}
|
|
4510
|
+
if (!opts.pipeline) {
|
|
4511
|
+
console.error(chalk.red('wait-for-pipeline: --pipeline <name> is required with the single-pair form (execution-id is only unique within a pipeline)'));
|
|
4512
|
+
process.exit(1);
|
|
4513
|
+
}
|
|
4514
|
+
validateExecutionEntry(opts.pipeline, executionId);
|
|
4515
|
+
executions = [{ pipeline: opts.pipeline, executionId }];
|
|
4516
|
+
}
|
|
4517
|
+
const waitOptions = resolveWaitOptions({
|
|
4518
|
+
timeout: opts.timeout,
|
|
4519
|
+
pollInterval: opts.pollInterval,
|
|
4520
|
+
region: opts.region,
|
|
4521
|
+
});
|
|
4522
|
+
// AwsRunner: shell out to the aws CLI, capturing stdout. On non-zero exit
|
|
4523
|
+
// execSync throws an Error carrying .stderr — the gate logic reads it.
|
|
4524
|
+
const aws = (args) => {
|
|
4525
|
+
const cmd = buildAwsCommand(args);
|
|
4526
|
+
return execSync(cmd, {
|
|
4527
|
+
encoding: 'utf-8',
|
|
4528
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4529
|
+
// Default 1MB can spuriously throw on a large get-pipeline-state
|
|
4530
|
+
// response; that would map to AWS_CALL_FAILED (exit 5) incorrectly.
|
|
4531
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
4532
|
+
});
|
|
4533
|
+
};
|
|
4534
|
+
const exitCode = await waitForPipelines(executions, waitOptions, { aws });
|
|
4535
|
+
process.exit(exitCode);
|
|
4536
|
+
}
|
|
4537
|
+
catch (err) {
|
|
4538
|
+
// Configuration / parse errors (bad --executions-json, bad --timeout, etc.)
|
|
4539
|
+
console.error(chalk.red(err.message));
|
|
4540
|
+
process.exit(1);
|
|
4541
|
+
}
|
|
4542
|
+
});
|
|
4478
4543
|
const step = pipelines.command('step').description('Pipeline step commands');
|
|
4479
4544
|
step
|
|
4480
4545
|
.command('list')
|
|
@@ -8173,21 +8238,26 @@ Examples:
|
|
|
8173
8238
|
});
|
|
8174
8239
|
// ── `fazemos dispatch` — role-to-role dispatch ─────────────────────
|
|
8175
8240
|
//
|
|
8176
|
-
//
|
|
8177
|
-
//
|
|
8178
|
-
//
|
|
8241
|
+
// F26 data flow (API write-through, Dex S-B):
|
|
8242
|
+
// 1. Resolve recipient via .fazemos/roles.json
|
|
8243
|
+
// 2. POST /api/dispatches (API writes DB row + fires fan-out + returns file_path)
|
|
8244
|
+
// 3. CLI writes inbox file at the returned file_path
|
|
8245
|
+
// 4. --commit git-adds the file
|
|
8179
8246
|
//
|
|
8180
|
-
//
|
|
8181
|
-
//
|
|
8182
|
-
//
|
|
8247
|
+
// Legacy / degraded mode (--file-only):
|
|
8248
|
+
// Skips the API call and writes only the local file.
|
|
8249
|
+
// Used by not-yet-migrated playbooks and offline work.
|
|
8183
8250
|
//
|
|
8184
8251
|
// Example:
|
|
8185
8252
|
// fazemos dispatch founder question --from business-strategist \
|
|
8186
8253
|
// --body "Should we ship F19 before F7?" --priority high --commit
|
|
8187
8254
|
program
|
|
8188
8255
|
.command('dispatch <to> <type>')
|
|
8189
|
-
.description(`
|
|
8190
|
-
|
|
8256
|
+
.description(`Send a role-to-role dispatch. Calls POST /api/dispatches (API writes DB row
|
|
8257
|
+
+ fires fan-out, returns file_path), then CLI writes the inbox file.
|
|
8258
|
+
|
|
8259
|
+
Use --file-only to skip the API and write only the local file (degraded/offline
|
|
8260
|
+
mode, for not-yet-migrated playbooks).
|
|
8191
8261
|
|
|
8192
8262
|
Recipients are resolved via the nearest .fazemos/roles.json registry, walking
|
|
8193
8263
|
up from the current directory. Cross-workspace recipients are followed via
|
|
@@ -8198,11 +8268,11 @@ Types: question | task | signal | response | flag | decision | direction`)
|
|
|
8198
8268
|
.option('--body <text>', 'markdown body of the dispatch (or use --body-file)')
|
|
8199
8269
|
.option('--body-file <path>', 'read body from a file')
|
|
8200
8270
|
.option('--priority <level>', 'low | normal | high', 'normal')
|
|
8201
|
-
.option('--re <ref>', 'optional reference (worksheet id,
|
|
8202
|
-
.option('--thread <id>', 'optional prior
|
|
8271
|
+
.option('--re <ref>', 'optional reference (worksheet id, feature token, etc.)')
|
|
8272
|
+
.option('--thread <id>', 'optional prior dispatch id this is threaded to')
|
|
8203
8273
|
.option('--expires-at <iso>', 'optional deadline (ISO 8601)')
|
|
8204
8274
|
.option('--commit', 'git add + commit the inbox file after writing')
|
|
8205
|
-
.option('--
|
|
8275
|
+
.option('--file-only', 'skip API call, write only the local inbox file (offline/legacy mode)')
|
|
8206
8276
|
.action(async (to, type, opts) => {
|
|
8207
8277
|
try {
|
|
8208
8278
|
// Validate type
|
|
@@ -8241,35 +8311,76 @@ Types: question | task | signal | response | flag | decision | direction`)
|
|
|
8241
8311
|
thread: opts.thread,
|
|
8242
8312
|
expiresAt: opts.expiresAt,
|
|
8243
8313
|
};
|
|
8244
|
-
// Build + write file
|
|
8245
8314
|
const { filename, content, summary } = buildInboxFile(input);
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
:
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8315
|
+
let fullPath;
|
|
8316
|
+
let relPath;
|
|
8317
|
+
if (opts.fileOnly) {
|
|
8318
|
+
// ── Legacy / offline mode: write file directly, skip API ────────
|
|
8319
|
+
fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
|
|
8320
|
+
relPath = fullPath.startsWith(registry._workspaceRoot)
|
|
8321
|
+
? fullPath.slice(registry._workspaceRoot.length + 1)
|
|
8322
|
+
: fullPath;
|
|
8323
|
+
console.log(chalk.green(`✓ Wrote inbox file (--file-only):`));
|
|
8324
|
+
console.log(` ${chalk.cyan(fullPath)}`);
|
|
8325
|
+
console.log(` Workspace: ${registry.workspace} Filler: ${role.filler.identity} (${role.filler.type})`);
|
|
8326
|
+
console.log(chalk.gray(' (API not called — file-only mode)'));
|
|
8327
|
+
}
|
|
8328
|
+
else {
|
|
8329
|
+
// ── F26 API write-through (Dex S-B) ─────────────────────────────
|
|
8330
|
+
const reqBody = {
|
|
8331
|
+
to,
|
|
8332
|
+
from: opts.from,
|
|
8333
|
+
type,
|
|
8334
|
+
priority: opts.priority ?? 'normal',
|
|
8335
|
+
body,
|
|
8336
|
+
thread_id: opts.thread ?? null,
|
|
8337
|
+
re: opts.re ? [opts.re] : null,
|
|
8338
|
+
expires_at: opts.expiresAt ?? null,
|
|
8339
|
+
source: 'cli',
|
|
8340
|
+
};
|
|
8341
|
+
let apiResp;
|
|
8255
8342
|
try {
|
|
8256
|
-
|
|
8257
|
-
const resp = await api('POST', '/api/notifications/dispatch', payload);
|
|
8258
|
-
if (resp.notified) {
|
|
8259
|
-
console.log(chalk.green(`✓ Notification fired: ${resp.channel}`));
|
|
8260
|
-
}
|
|
8261
|
-
else {
|
|
8262
|
-
console.log(chalk.gray(` (no notification fired — ${resp.reason ?? 'unknown'})`));
|
|
8263
|
-
}
|
|
8343
|
+
apiResp = await api('POST', '/api/dispatches', reqBody);
|
|
8264
8344
|
}
|
|
8265
8345
|
catch (err) {
|
|
8346
|
+
// If API is unavailable, fall back to file-only with a warning
|
|
8266
8347
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8267
|
-
console.log(chalk.yellow(` warning:
|
|
8268
|
-
|
|
8348
|
+
console.log(chalk.yellow(` warning: API call failed (${msg}), falling back to --file-only`));
|
|
8349
|
+
fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
|
|
8350
|
+
relPath = fullPath.startsWith(registry._workspaceRoot)
|
|
8351
|
+
? fullPath.slice(registry._workspaceRoot.length + 1)
|
|
8352
|
+
: fullPath;
|
|
8353
|
+
console.log(chalk.green(`✓ Wrote inbox file (fallback):`));
|
|
8354
|
+
console.log(` ${chalk.cyan(fullPath)}`);
|
|
8355
|
+
if (opts.commit) {
|
|
8356
|
+
try {
|
|
8357
|
+
gitCommitInboxFile(registry._workspaceRoot, relPath, `dispatch(${opts.from} → ${to}): ${type}`);
|
|
8358
|
+
console.log(chalk.green('✓ Committed.'));
|
|
8359
|
+
}
|
|
8360
|
+
catch (cerr) {
|
|
8361
|
+
console.log(chalk.yellow(` warning: commit failed — ${cerr instanceof Error ? cerr.message : String(cerr)}`));
|
|
8362
|
+
}
|
|
8363
|
+
}
|
|
8364
|
+
return;
|
|
8365
|
+
}
|
|
8366
|
+
// API returned file_path — CLI writes the actual file (Dex S-B)
|
|
8367
|
+
if (apiResp.file_path) {
|
|
8368
|
+
fullPath = writeInboxFileAtPath(registry._workspaceRoot, apiResp.file_path, content);
|
|
8369
|
+
relPath = apiResp.file_path;
|
|
8370
|
+
}
|
|
8371
|
+
else {
|
|
8372
|
+
// API didn't return a file_path (e.g. cross-workspace agent recipient) — write locally
|
|
8373
|
+
fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
|
|
8374
|
+
relPath = fullPath.startsWith(registry._workspaceRoot)
|
|
8375
|
+
? fullPath.slice(registry._workspaceRoot.length + 1)
|
|
8376
|
+
: fullPath;
|
|
8377
|
+
}
|
|
8378
|
+
console.log(chalk.green(`✓ Dispatch created (id: ${apiResp.id}):`));
|
|
8379
|
+
console.log(` ${chalk.cyan(fullPath)}`);
|
|
8380
|
+
console.log(` Workspace: ${registry.workspace} Filler: ${role.filler.identity} (${role.filler.type})`);
|
|
8381
|
+
if (apiResp.thread_id && apiResp.thread_id !== apiResp.id) {
|
|
8382
|
+
console.log(` Thread: ${apiResp.thread_id}`);
|
|
8269
8383
|
}
|
|
8270
|
-
}
|
|
8271
|
-
else {
|
|
8272
|
-
console.log(chalk.gray(' (--no-notify; API not called)'));
|
|
8273
8384
|
}
|
|
8274
8385
|
// Commit if requested
|
|
8275
8386
|
if (opts.commit) {
|
|
@@ -8288,6 +8399,156 @@ Types: question | task | signal | response | flag | decision | direction`)
|
|
|
8288
8399
|
process.exit(1);
|
|
8289
8400
|
}
|
|
8290
8401
|
});
|
|
8402
|
+
// ── `fazemos roles sync` — upsert roles.json into role_registrations ──
|
|
8403
|
+
//
|
|
8404
|
+
// F26 (Dex M2): reads cognito_sub from roles.json filler entries and
|
|
8405
|
+
// POSTs to POST /api/role-registrations/sync. Fails loud if a human
|
|
8406
|
+
// filler lacks cognito_sub (required for inbox mapping).
|
|
8407
|
+
program
|
|
8408
|
+
.command('roles sync')
|
|
8409
|
+
.description(`Upsert roles from the nearest .fazemos/roles.json into role_registrations
|
|
8410
|
+
via POST /api/role-registrations/sync.
|
|
8411
|
+
|
|
8412
|
+
Human fillers MUST have cognito_sub set in roles.json (Dex M2). The command
|
|
8413
|
+
fails with a clear error if any human filler is missing it.
|
|
8414
|
+
|
|
8415
|
+
Run after editing roles.json to sync inbox access to the API.`)
|
|
8416
|
+
.option('--org-id <id>', 'explicit org ID (defaults to active org from config)')
|
|
8417
|
+
.option('--project-id <id>', 'explicit project ID (defaults to active project from config)')
|
|
8418
|
+
.option('--dry-run', 'build and print the payload without sending to the API')
|
|
8419
|
+
.action(async (opts) => {
|
|
8420
|
+
try {
|
|
8421
|
+
const localRegistry = findLocalRegistry(process.cwd());
|
|
8422
|
+
if (!localRegistry) {
|
|
8423
|
+
throw new Error(`No .fazemos/roles.json found in cwd or any parent. ` +
|
|
8424
|
+
`Run from inside a Fazemos-aware workspace.`);
|
|
8425
|
+
}
|
|
8426
|
+
const orgId = opts.orgId ?? getActiveOrgId() ?? null;
|
|
8427
|
+
const projectId = opts.projectId ?? getActiveProjectId() ?? null;
|
|
8428
|
+
// buildRolesSyncPayload validates cognito_sub and throws loud if missing
|
|
8429
|
+
const payload = buildRolesSyncPayload(localRegistry, orgId, projectId);
|
|
8430
|
+
if (opts.dryRun) {
|
|
8431
|
+
console.log(chalk.cyan('Dry run — payload that would be sent:'));
|
|
8432
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
8433
|
+
return;
|
|
8434
|
+
}
|
|
8435
|
+
const result = await api('POST', '/api/role-registrations/sync', payload, { noProjectHeader: true });
|
|
8436
|
+
console.log(chalk.green(`✓ Roles synced: ${result.synced} registration(s) upserted`));
|
|
8437
|
+
if (result.roles?.length) {
|
|
8438
|
+
for (const slug of result.roles) {
|
|
8439
|
+
console.log(` ${chalk.cyan(slug)}`);
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
catch (err) {
|
|
8444
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
8445
|
+
process.exit(1);
|
|
8446
|
+
}
|
|
8447
|
+
});
|
|
8448
|
+
// ── `fazemos dispatch-backfill` — phase-B one-time import ────────────
|
|
8449
|
+
//
|
|
8450
|
+
// F26: Imports UNPROCESSED inbox files as dispatches with source='migration'.
|
|
8451
|
+
// Idempotent on content hash — files already imported are skipped.
|
|
8452
|
+
// Used once to seed the DB from the existing file-based inbox history.
|
|
8453
|
+
// NOTE: hyphenated to avoid Commander conflict with `dispatch <to> <type>`.
|
|
8454
|
+
program
|
|
8455
|
+
.command('dispatch-backfill')
|
|
8456
|
+
.description(`Phase-B one-time import of unprocessed inbox files as dispatches
|
|
8457
|
+
(status=unread, source=migration). Idempotent: files already imported
|
|
8458
|
+
are skipped (content-hash dedup). Skips any file that has a corresponding
|
|
8459
|
+
entry in the role's processed/ directory.
|
|
8460
|
+
|
|
8461
|
+
Run from the workspace root where roles/*/inbox/ directories live.`)
|
|
8462
|
+
.option('--roles-dir <path>', 'relative path to roles directory', 'roles')
|
|
8463
|
+
.option('--dry-run', 'list files that would be imported without sending to API')
|
|
8464
|
+
.option('--limit <n>', 'max files to import in one run', (v) => parseInt(v, 10))
|
|
8465
|
+
.action(async (opts) => {
|
|
8466
|
+
try {
|
|
8467
|
+
const localRegistry = findLocalRegistry(process.cwd());
|
|
8468
|
+
if (!localRegistry) {
|
|
8469
|
+
throw new Error(`No .fazemos/roles.json found in cwd or any parent. ` +
|
|
8470
|
+
`Run from inside a Fazemos-aware workspace.`);
|
|
8471
|
+
}
|
|
8472
|
+
const files = findUnprocessedInboxFiles(localRegistry._workspaceRoot, opts.rolesDir ?? 'roles');
|
|
8473
|
+
if (files.length === 0) {
|
|
8474
|
+
console.log(chalk.gray('No unprocessed inbox files found.'));
|
|
8475
|
+
return;
|
|
8476
|
+
}
|
|
8477
|
+
const limit = opts.limit ?? files.length;
|
|
8478
|
+
const toProcess = files.slice(0, limit);
|
|
8479
|
+
if (opts.dryRun) {
|
|
8480
|
+
console.log(chalk.cyan(`Dry run — ${toProcess.length} file(s) would be imported:`));
|
|
8481
|
+
for (const f of toProcess) {
|
|
8482
|
+
const hash = computeFileHash(f.content);
|
|
8483
|
+
console.log(` ${chalk.gray(hash.slice(0, 8))} ${f.relativePath}`);
|
|
8484
|
+
}
|
|
8485
|
+
return;
|
|
8486
|
+
}
|
|
8487
|
+
let imported = 0;
|
|
8488
|
+
let skipped = 0;
|
|
8489
|
+
const errors = [];
|
|
8490
|
+
for (const f of toProcess) {
|
|
8491
|
+
try {
|
|
8492
|
+
// Extract frontmatter fields for the API call
|
|
8493
|
+
const fmMatch = f.content.match(/^---\n([\s\S]*?)\n---/);
|
|
8494
|
+
let from = f.rolePath; // fallback: role dir name
|
|
8495
|
+
let to = f.rolePath;
|
|
8496
|
+
let type = 'signal';
|
|
8497
|
+
let priority = 'normal';
|
|
8498
|
+
if (fmMatch) {
|
|
8499
|
+
const fm = fmMatch[1];
|
|
8500
|
+
const fromM = fm.match(/^from:\s*(.+)$/m);
|
|
8501
|
+
const toM = fm.match(/^to:\s*(.+)$/m);
|
|
8502
|
+
const typeM = fm.match(/^type:\s*(.+)$/m);
|
|
8503
|
+
const prioM = fm.match(/^priority:\s*(.+)$/m);
|
|
8504
|
+
if (fromM)
|
|
8505
|
+
from = fromM[1].trim();
|
|
8506
|
+
if (toM)
|
|
8507
|
+
to = toM[1].trim();
|
|
8508
|
+
if (typeM)
|
|
8509
|
+
type = typeM[1].trim();
|
|
8510
|
+
if (prioM)
|
|
8511
|
+
priority = prioM[1].trim();
|
|
8512
|
+
}
|
|
8513
|
+
// Body = content after the closing ---
|
|
8514
|
+
const bodyMatch = f.content.match(/^---\n[\s\S]*?\n---\n\n?([\s\S]*)$/);
|
|
8515
|
+
const body = bodyMatch ? bodyMatch[1].trim() : f.content.trim();
|
|
8516
|
+
const reqBody = {
|
|
8517
|
+
to,
|
|
8518
|
+
from,
|
|
8519
|
+
type,
|
|
8520
|
+
priority,
|
|
8521
|
+
body,
|
|
8522
|
+
no_notify: true, // backfill is historical — never fire Slack fan-out
|
|
8523
|
+
source: 'migration',
|
|
8524
|
+
};
|
|
8525
|
+
await api('POST', '/api/dispatches', reqBody);
|
|
8526
|
+
imported++;
|
|
8527
|
+
console.log(chalk.green(` ✓ ${f.relativePath}`));
|
|
8528
|
+
}
|
|
8529
|
+
catch (err) {
|
|
8530
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8531
|
+
// 409 = already imported (content hash dedup on server side)
|
|
8532
|
+
if (msg.includes('409') || msg.toLowerCase().includes('already imported')) {
|
|
8533
|
+
skipped++;
|
|
8534
|
+
console.log(chalk.gray(` ~ ${f.relativePath} (already imported)`));
|
|
8535
|
+
}
|
|
8536
|
+
else {
|
|
8537
|
+
errors.push(`${f.relativePath}: ${msg}`);
|
|
8538
|
+
console.log(chalk.yellow(` ✗ ${f.relativePath}: ${msg}`));
|
|
8539
|
+
}
|
|
8540
|
+
}
|
|
8541
|
+
}
|
|
8542
|
+
console.log();
|
|
8543
|
+
console.log(`Backfill complete: ${chalk.green(String(imported))} imported, ${chalk.gray(String(skipped))} skipped, ${errors.length > 0 ? chalk.red(String(errors.length)) : '0'} errors`);
|
|
8544
|
+
if (errors.length > 0)
|
|
8545
|
+
process.exit(1);
|
|
8546
|
+
}
|
|
8547
|
+
catch (err) {
|
|
8548
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
8549
|
+
process.exit(1);
|
|
8550
|
+
}
|
|
8551
|
+
});
|
|
8291
8552
|
program
|
|
8292
8553
|
.command('dispatch-list-roles')
|
|
8293
8554
|
.description('List roles available in the nearest .fazemos/roles.json registry')
|