@formigio/fazemos-cli 0.10.16 → 0.10.19
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 +350 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ 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
15
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
@@ -5539,14 +5539,57 @@ executions
|
|
|
5539
5539
|
// ── My Work ─────────────────────────────────────────────────
|
|
5540
5540
|
program
|
|
5541
5541
|
.command('my-work')
|
|
5542
|
-
.description('Show pending work in the active project (or all projects with --all-projects)')
|
|
5542
|
+
.description('Show pending work in the active project (or all projects with --all-projects, or across every org with --all-orgs)')
|
|
5543
5543
|
.option('--project <slug>', 'Override active project for this call')
|
|
5544
5544
|
.option('--all-projects', 'Show pending work across every project in the active org', false)
|
|
5545
|
+
.option('-a, --all-orgs', 'Show pending work across every org you are a member of (calls /api/my-work/unified)', false)
|
|
5546
|
+
.option('--json', 'Emit machine-readable JSON', false)
|
|
5545
5547
|
.action(async (opts) => {
|
|
5548
|
+
// ── F19 — cross-Org unified path ───────────────────────────
|
|
5549
|
+
// When --all-orgs is set, we drop X-Org-Id / X-Fazemos-Project-Id
|
|
5550
|
+
// headers and call the unified endpoint. The endpoint is mounted
|
|
5551
|
+
// BEFORE requireOrgMembership and runs across every Org the caller is
|
|
5552
|
+
// an active member of. See rul_cross_org_auth_no_org_middleware.
|
|
5553
|
+
if (opts.allOrgs) {
|
|
5554
|
+
try {
|
|
5555
|
+
const data = await api('GET', '/api/my-work/unified', undefined, { noProjectHeader: true });
|
|
5556
|
+
if (opts.json) {
|
|
5557
|
+
console.log(JSON.stringify(data, null, 2));
|
|
5558
|
+
return;
|
|
5559
|
+
}
|
|
5560
|
+
// Sage edge case #24: single-Org user with --all-orgs gets a soft
|
|
5561
|
+
// notice + their normal output (no flag-stripping, no error).
|
|
5562
|
+
if (data.meta.orgCount === 1) {
|
|
5563
|
+
console.log(chalk.yellow("--all-orgs has no effect — you're a member of 1 org. Showing your current scope.\n"));
|
|
5564
|
+
}
|
|
5565
|
+
renderUnifiedMyWork(data);
|
|
5566
|
+
return;
|
|
5567
|
+
}
|
|
5568
|
+
catch (err) {
|
|
5569
|
+
// Defensive: 403 NO_ACTIVE_MEMBERSHIP gets a friendly nudge.
|
|
5570
|
+
if (err instanceof ApiError && err.code === 'NO_ACTIVE_MEMBERSHIP') {
|
|
5571
|
+
console.error(chalk.red('No active org memberships.'));
|
|
5572
|
+
console.error(chalk.gray('Ask an admin to add you, or accept a pending invite.'));
|
|
5573
|
+
process.exit(1);
|
|
5574
|
+
}
|
|
5575
|
+
if (err instanceof Error) {
|
|
5576
|
+
console.error(chalk.red(err.message));
|
|
5577
|
+
}
|
|
5578
|
+
else {
|
|
5579
|
+
console.error(chalk.red(String(err)));
|
|
5580
|
+
}
|
|
5581
|
+
process.exit(1);
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
// ── Default project-scoped path (unchanged) ─────────────────
|
|
5546
5585
|
try {
|
|
5547
5586
|
const data = await api('GET', '/api/my-work', undefined, projectOpts(opts));
|
|
5548
5587
|
const c = data.commitments;
|
|
5549
5588
|
const total = c.overdue.length + c.due_today.length + c.due_this_week.length + c.upcoming.length;
|
|
5589
|
+
if (opts.json) {
|
|
5590
|
+
console.log(JSON.stringify(data, null, 2));
|
|
5591
|
+
return;
|
|
5592
|
+
}
|
|
5550
5593
|
console.log(chalk.cyan(`Commitments: ${total}`));
|
|
5551
5594
|
if (c.overdue.length)
|
|
5552
5595
|
console.log(chalk.red(` Overdue: ${c.overdue.length}`));
|
|
@@ -5564,6 +5607,81 @@ program
|
|
|
5564
5607
|
handleScopedError(err);
|
|
5565
5608
|
}
|
|
5566
5609
|
});
|
|
5610
|
+
function renderUnifiedMyWork(data) {
|
|
5611
|
+
if (data.items.length === 0) {
|
|
5612
|
+
console.log(chalk.green("Looking good — across every project, in every org, you're caught up."));
|
|
5613
|
+
return;
|
|
5614
|
+
}
|
|
5615
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
5616
|
+
const truncate = (s, n) => s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
5617
|
+
// Header
|
|
5618
|
+
const header = [
|
|
5619
|
+
pad('STATUS', 10),
|
|
5620
|
+
pad('TYPE', 9),
|
|
5621
|
+
pad('TITLE', 36),
|
|
5622
|
+
pad('ORG', 5),
|
|
5623
|
+
pad('PROJ', 5),
|
|
5624
|
+
'DUE',
|
|
5625
|
+
].join(' ');
|
|
5626
|
+
console.log(chalk.gray(header));
|
|
5627
|
+
console.log(chalk.gray('-'.repeat(80)));
|
|
5628
|
+
for (const item of data.items) {
|
|
5629
|
+
// Status color rules per manifest.cli.changes.
|
|
5630
|
+
let statusLabel;
|
|
5631
|
+
if (item.dueDate && item.dueDate < today) {
|
|
5632
|
+
statusLabel = chalk.red('overdue');
|
|
5633
|
+
}
|
|
5634
|
+
else if (item.dueDate === today) {
|
|
5635
|
+
statusLabel = chalk.yellow('today');
|
|
5636
|
+
}
|
|
5637
|
+
else {
|
|
5638
|
+
statusLabel = item.status;
|
|
5639
|
+
}
|
|
5640
|
+
const row = [
|
|
5641
|
+
pad(statusLabel, 10),
|
|
5642
|
+
pad(item.type, 9),
|
|
5643
|
+
pad(truncate(item.title, 36), 36),
|
|
5644
|
+
pad(`[${computeAbbreviation(item.org.name)}]`, 5),
|
|
5645
|
+
pad(`[${computeAbbreviation(item.project.name)}]`, 5),
|
|
5646
|
+
item.dueDate ?? '—',
|
|
5647
|
+
].join(' ');
|
|
5648
|
+
console.log(row);
|
|
5649
|
+
}
|
|
5650
|
+
// Footer
|
|
5651
|
+
const shown = data.items.length;
|
|
5652
|
+
const total = data.meta.typeCounts.commitment +
|
|
5653
|
+
data.meta.typeCounts.action +
|
|
5654
|
+
data.meta.typeCounts.worksheet +
|
|
5655
|
+
data.meta.typeCounts.check_in +
|
|
5656
|
+
data.meta.typeCounts.pipeline_step;
|
|
5657
|
+
console.log('');
|
|
5658
|
+
console.log(chalk.gray(`Showing ${shown} of ${total}.` +
|
|
5659
|
+
(data.nextCursor ? ' More available — re-run with --cursor=<next>.' : '')));
|
|
5660
|
+
console.log(chalk.gray(`Tip: fazemos my-work (no flag) shows just the active project.`));
|
|
5661
|
+
}
|
|
5662
|
+
/**
|
|
5663
|
+
* 2-letter abbreviation: first letter of first word + first letter of next
|
|
5664
|
+
* word, or first 2 letters of the only word if no second word exists.
|
|
5665
|
+
* Duplicates the helper used in fazemos-web (ProjectBadge.tsx) per
|
|
5666
|
+
* manifest.cli.shared_helpers — "don't block on packaging."
|
|
5667
|
+
*/
|
|
5668
|
+
function computeAbbreviation(name) {
|
|
5669
|
+
const cleaned = name.trim();
|
|
5670
|
+
if (!cleaned)
|
|
5671
|
+
return '??';
|
|
5672
|
+
const words = cleaned.split(/\s+/);
|
|
5673
|
+
if (words.length === 1) {
|
|
5674
|
+
return words[0].slice(0, 2).toUpperCase();
|
|
5675
|
+
}
|
|
5676
|
+
return (words[0][0] + words[1][0]).toUpperCase();
|
|
5677
|
+
}
|
|
5678
|
+
function pad(s, width) {
|
|
5679
|
+
// Strip ANSI escape codes for length measurement so colored cells line up.
|
|
5680
|
+
const visible = s.replace(/\u001b\[[0-9;]*m/g, '');
|
|
5681
|
+
if (visible.length >= width)
|
|
5682
|
+
return s;
|
|
5683
|
+
return s + ' '.repeat(width - visible.length);
|
|
5684
|
+
}
|
|
5567
5685
|
// ── Test ────────────────────────────────────────────────────
|
|
5568
5686
|
program
|
|
5569
5687
|
.command('test')
|
|
@@ -8055,21 +8173,26 @@ Examples:
|
|
|
8055
8173
|
});
|
|
8056
8174
|
// ── `fazemos dispatch` — role-to-role dispatch ─────────────────────
|
|
8057
8175
|
//
|
|
8058
|
-
//
|
|
8059
|
-
//
|
|
8060
|
-
//
|
|
8176
|
+
// F26 data flow (API write-through, Dex S-B):
|
|
8177
|
+
// 1. Resolve recipient via .fazemos/roles.json
|
|
8178
|
+
// 2. POST /api/dispatches (API writes DB row + fires fan-out + returns file_path)
|
|
8179
|
+
// 3. CLI writes inbox file at the returned file_path
|
|
8180
|
+
// 4. --commit git-adds the file
|
|
8061
8181
|
//
|
|
8062
|
-
//
|
|
8063
|
-
//
|
|
8064
|
-
//
|
|
8182
|
+
// Legacy / degraded mode (--file-only):
|
|
8183
|
+
// Skips the API call and writes only the local file.
|
|
8184
|
+
// Used by not-yet-migrated playbooks and offline work.
|
|
8065
8185
|
//
|
|
8066
8186
|
// Example:
|
|
8067
8187
|
// fazemos dispatch founder question --from business-strategist \
|
|
8068
8188
|
// --body "Should we ship F19 before F7?" --priority high --commit
|
|
8069
8189
|
program
|
|
8070
8190
|
.command('dispatch <to> <type>')
|
|
8071
|
-
.description(`
|
|
8072
|
-
|
|
8191
|
+
.description(`Send a role-to-role dispatch. Calls POST /api/dispatches (API writes DB row
|
|
8192
|
+
+ fires fan-out, returns file_path), then CLI writes the inbox file.
|
|
8193
|
+
|
|
8194
|
+
Use --file-only to skip the API and write only the local file (degraded/offline
|
|
8195
|
+
mode, for not-yet-migrated playbooks).
|
|
8073
8196
|
|
|
8074
8197
|
Recipients are resolved via the nearest .fazemos/roles.json registry, walking
|
|
8075
8198
|
up from the current directory. Cross-workspace recipients are followed via
|
|
@@ -8080,11 +8203,11 @@ Types: question | task | signal | response | flag | decision | direction`)
|
|
|
8080
8203
|
.option('--body <text>', 'markdown body of the dispatch (or use --body-file)')
|
|
8081
8204
|
.option('--body-file <path>', 'read body from a file')
|
|
8082
8205
|
.option('--priority <level>', 'low | normal | high', 'normal')
|
|
8083
|
-
.option('--re <ref>', 'optional reference (worksheet id,
|
|
8084
|
-
.option('--thread <id>', 'optional prior
|
|
8206
|
+
.option('--re <ref>', 'optional reference (worksheet id, feature token, etc.)')
|
|
8207
|
+
.option('--thread <id>', 'optional prior dispatch id this is threaded to')
|
|
8085
8208
|
.option('--expires-at <iso>', 'optional deadline (ISO 8601)')
|
|
8086
8209
|
.option('--commit', 'git add + commit the inbox file after writing')
|
|
8087
|
-
.option('--
|
|
8210
|
+
.option('--file-only', 'skip API call, write only the local inbox file (offline/legacy mode)')
|
|
8088
8211
|
.action(async (to, type, opts) => {
|
|
8089
8212
|
try {
|
|
8090
8213
|
// Validate type
|
|
@@ -8123,35 +8246,76 @@ Types: question | task | signal | response | flag | decision | direction`)
|
|
|
8123
8246
|
thread: opts.thread,
|
|
8124
8247
|
expiresAt: opts.expiresAt,
|
|
8125
8248
|
};
|
|
8126
|
-
// Build + write file
|
|
8127
8249
|
const { filename, content, summary } = buildInboxFile(input);
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
:
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8250
|
+
let fullPath;
|
|
8251
|
+
let relPath;
|
|
8252
|
+
if (opts.fileOnly) {
|
|
8253
|
+
// ── Legacy / offline mode: write file directly, skip API ────────
|
|
8254
|
+
fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
|
|
8255
|
+
relPath = fullPath.startsWith(registry._workspaceRoot)
|
|
8256
|
+
? fullPath.slice(registry._workspaceRoot.length + 1)
|
|
8257
|
+
: fullPath;
|
|
8258
|
+
console.log(chalk.green(`✓ Wrote inbox file (--file-only):`));
|
|
8259
|
+
console.log(` ${chalk.cyan(fullPath)}`);
|
|
8260
|
+
console.log(` Workspace: ${registry.workspace} Filler: ${role.filler.identity} (${role.filler.type})`);
|
|
8261
|
+
console.log(chalk.gray(' (API not called — file-only mode)'));
|
|
8262
|
+
}
|
|
8263
|
+
else {
|
|
8264
|
+
// ── F26 API write-through (Dex S-B) ─────────────────────────────
|
|
8265
|
+
const reqBody = {
|
|
8266
|
+
to,
|
|
8267
|
+
from: opts.from,
|
|
8268
|
+
type,
|
|
8269
|
+
priority: opts.priority ?? 'normal',
|
|
8270
|
+
body,
|
|
8271
|
+
thread_id: opts.thread ?? null,
|
|
8272
|
+
re: opts.re ? [opts.re] : null,
|
|
8273
|
+
expires_at: opts.expiresAt ?? null,
|
|
8274
|
+
source: 'cli',
|
|
8275
|
+
};
|
|
8276
|
+
let apiResp;
|
|
8137
8277
|
try {
|
|
8138
|
-
|
|
8139
|
-
const resp = await api('POST', '/api/notifications/dispatch', payload);
|
|
8140
|
-
if (resp.notified) {
|
|
8141
|
-
console.log(chalk.green(`✓ Notification fired: ${resp.channel}`));
|
|
8142
|
-
}
|
|
8143
|
-
else {
|
|
8144
|
-
console.log(chalk.gray(` (no notification fired — ${resp.reason ?? 'unknown'})`));
|
|
8145
|
-
}
|
|
8278
|
+
apiResp = await api('POST', '/api/dispatches', reqBody);
|
|
8146
8279
|
}
|
|
8147
8280
|
catch (err) {
|
|
8281
|
+
// If API is unavailable, fall back to file-only with a warning
|
|
8148
8282
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8149
|
-
console.log(chalk.yellow(` warning:
|
|
8150
|
-
|
|
8283
|
+
console.log(chalk.yellow(` warning: API call failed (${msg}), falling back to --file-only`));
|
|
8284
|
+
fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
|
|
8285
|
+
relPath = fullPath.startsWith(registry._workspaceRoot)
|
|
8286
|
+
? fullPath.slice(registry._workspaceRoot.length + 1)
|
|
8287
|
+
: fullPath;
|
|
8288
|
+
console.log(chalk.green(`✓ Wrote inbox file (fallback):`));
|
|
8289
|
+
console.log(` ${chalk.cyan(fullPath)}`);
|
|
8290
|
+
if (opts.commit) {
|
|
8291
|
+
try {
|
|
8292
|
+
gitCommitInboxFile(registry._workspaceRoot, relPath, `dispatch(${opts.from} → ${to}): ${type}`);
|
|
8293
|
+
console.log(chalk.green('✓ Committed.'));
|
|
8294
|
+
}
|
|
8295
|
+
catch (cerr) {
|
|
8296
|
+
console.log(chalk.yellow(` warning: commit failed — ${cerr instanceof Error ? cerr.message : String(cerr)}`));
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
return;
|
|
8300
|
+
}
|
|
8301
|
+
// API returned file_path — CLI writes the actual file (Dex S-B)
|
|
8302
|
+
if (apiResp.file_path) {
|
|
8303
|
+
fullPath = writeInboxFileAtPath(registry._workspaceRoot, apiResp.file_path, content);
|
|
8304
|
+
relPath = apiResp.file_path;
|
|
8305
|
+
}
|
|
8306
|
+
else {
|
|
8307
|
+
// API didn't return a file_path (e.g. cross-workspace agent recipient) — write locally
|
|
8308
|
+
fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
|
|
8309
|
+
relPath = fullPath.startsWith(registry._workspaceRoot)
|
|
8310
|
+
? fullPath.slice(registry._workspaceRoot.length + 1)
|
|
8311
|
+
: fullPath;
|
|
8312
|
+
}
|
|
8313
|
+
console.log(chalk.green(`✓ Dispatch created (id: ${apiResp.id}):`));
|
|
8314
|
+
console.log(` ${chalk.cyan(fullPath)}`);
|
|
8315
|
+
console.log(` Workspace: ${registry.workspace} Filler: ${role.filler.identity} (${role.filler.type})`);
|
|
8316
|
+
if (apiResp.thread_id && apiResp.thread_id !== apiResp.id) {
|
|
8317
|
+
console.log(` Thread: ${apiResp.thread_id}`);
|
|
8151
8318
|
}
|
|
8152
|
-
}
|
|
8153
|
-
else {
|
|
8154
|
-
console.log(chalk.gray(' (--no-notify; API not called)'));
|
|
8155
8319
|
}
|
|
8156
8320
|
// Commit if requested
|
|
8157
8321
|
if (opts.commit) {
|
|
@@ -8170,6 +8334,156 @@ Types: question | task | signal | response | flag | decision | direction`)
|
|
|
8170
8334
|
process.exit(1);
|
|
8171
8335
|
}
|
|
8172
8336
|
});
|
|
8337
|
+
// ── `fazemos roles sync` — upsert roles.json into role_registrations ──
|
|
8338
|
+
//
|
|
8339
|
+
// F26 (Dex M2): reads cognito_sub from roles.json filler entries and
|
|
8340
|
+
// POSTs to POST /api/role-registrations/sync. Fails loud if a human
|
|
8341
|
+
// filler lacks cognito_sub (required for inbox mapping).
|
|
8342
|
+
program
|
|
8343
|
+
.command('roles sync')
|
|
8344
|
+
.description(`Upsert roles from the nearest .fazemos/roles.json into role_registrations
|
|
8345
|
+
via POST /api/role-registrations/sync.
|
|
8346
|
+
|
|
8347
|
+
Human fillers MUST have cognito_sub set in roles.json (Dex M2). The command
|
|
8348
|
+
fails with a clear error if any human filler is missing it.
|
|
8349
|
+
|
|
8350
|
+
Run after editing roles.json to sync inbox access to the API.`)
|
|
8351
|
+
.option('--org-id <id>', 'explicit org ID (defaults to active org from config)')
|
|
8352
|
+
.option('--project-id <id>', 'explicit project ID (defaults to active project from config)')
|
|
8353
|
+
.option('--dry-run', 'build and print the payload without sending to the API')
|
|
8354
|
+
.action(async (opts) => {
|
|
8355
|
+
try {
|
|
8356
|
+
const localRegistry = findLocalRegistry(process.cwd());
|
|
8357
|
+
if (!localRegistry) {
|
|
8358
|
+
throw new Error(`No .fazemos/roles.json found in cwd or any parent. ` +
|
|
8359
|
+
`Run from inside a Fazemos-aware workspace.`);
|
|
8360
|
+
}
|
|
8361
|
+
const orgId = opts.orgId ?? getActiveOrgId() ?? null;
|
|
8362
|
+
const projectId = opts.projectId ?? getActiveProjectId() ?? null;
|
|
8363
|
+
// buildRolesSyncPayload validates cognito_sub and throws loud if missing
|
|
8364
|
+
const payload = buildRolesSyncPayload(localRegistry, orgId, projectId);
|
|
8365
|
+
if (opts.dryRun) {
|
|
8366
|
+
console.log(chalk.cyan('Dry run — payload that would be sent:'));
|
|
8367
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
8368
|
+
return;
|
|
8369
|
+
}
|
|
8370
|
+
const result = await api('POST', '/api/role-registrations/sync', payload, { noProjectHeader: true });
|
|
8371
|
+
console.log(chalk.green(`✓ Roles synced: ${result.synced} registration(s) upserted`));
|
|
8372
|
+
if (result.roles?.length) {
|
|
8373
|
+
for (const slug of result.roles) {
|
|
8374
|
+
console.log(` ${chalk.cyan(slug)}`);
|
|
8375
|
+
}
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
catch (err) {
|
|
8379
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
8380
|
+
process.exit(1);
|
|
8381
|
+
}
|
|
8382
|
+
});
|
|
8383
|
+
// ── `fazemos dispatch-backfill` — phase-B one-time import ────────────
|
|
8384
|
+
//
|
|
8385
|
+
// F26: Imports UNPROCESSED inbox files as dispatches with source='migration'.
|
|
8386
|
+
// Idempotent on content hash — files already imported are skipped.
|
|
8387
|
+
// Used once to seed the DB from the existing file-based inbox history.
|
|
8388
|
+
// NOTE: hyphenated to avoid Commander conflict with `dispatch <to> <type>`.
|
|
8389
|
+
program
|
|
8390
|
+
.command('dispatch-backfill')
|
|
8391
|
+
.description(`Phase-B one-time import of unprocessed inbox files as dispatches
|
|
8392
|
+
(status=unread, source=migration). Idempotent: files already imported
|
|
8393
|
+
are skipped (content-hash dedup). Skips any file that has a corresponding
|
|
8394
|
+
entry in the role's processed/ directory.
|
|
8395
|
+
|
|
8396
|
+
Run from the workspace root where roles/*/inbox/ directories live.`)
|
|
8397
|
+
.option('--roles-dir <path>', 'relative path to roles directory', 'roles')
|
|
8398
|
+
.option('--dry-run', 'list files that would be imported without sending to API')
|
|
8399
|
+
.option('--limit <n>', 'max files to import in one run', (v) => parseInt(v, 10))
|
|
8400
|
+
.action(async (opts) => {
|
|
8401
|
+
try {
|
|
8402
|
+
const localRegistry = findLocalRegistry(process.cwd());
|
|
8403
|
+
if (!localRegistry) {
|
|
8404
|
+
throw new Error(`No .fazemos/roles.json found in cwd or any parent. ` +
|
|
8405
|
+
`Run from inside a Fazemos-aware workspace.`);
|
|
8406
|
+
}
|
|
8407
|
+
const files = findUnprocessedInboxFiles(localRegistry._workspaceRoot, opts.rolesDir ?? 'roles');
|
|
8408
|
+
if (files.length === 0) {
|
|
8409
|
+
console.log(chalk.gray('No unprocessed inbox files found.'));
|
|
8410
|
+
return;
|
|
8411
|
+
}
|
|
8412
|
+
const limit = opts.limit ?? files.length;
|
|
8413
|
+
const toProcess = files.slice(0, limit);
|
|
8414
|
+
if (opts.dryRun) {
|
|
8415
|
+
console.log(chalk.cyan(`Dry run — ${toProcess.length} file(s) would be imported:`));
|
|
8416
|
+
for (const f of toProcess) {
|
|
8417
|
+
const hash = computeFileHash(f.content);
|
|
8418
|
+
console.log(` ${chalk.gray(hash.slice(0, 8))} ${f.relativePath}`);
|
|
8419
|
+
}
|
|
8420
|
+
return;
|
|
8421
|
+
}
|
|
8422
|
+
let imported = 0;
|
|
8423
|
+
let skipped = 0;
|
|
8424
|
+
const errors = [];
|
|
8425
|
+
for (const f of toProcess) {
|
|
8426
|
+
try {
|
|
8427
|
+
// Extract frontmatter fields for the API call
|
|
8428
|
+
const fmMatch = f.content.match(/^---\n([\s\S]*?)\n---/);
|
|
8429
|
+
let from = f.rolePath; // fallback: role dir name
|
|
8430
|
+
let to = f.rolePath;
|
|
8431
|
+
let type = 'signal';
|
|
8432
|
+
let priority = 'normal';
|
|
8433
|
+
if (fmMatch) {
|
|
8434
|
+
const fm = fmMatch[1];
|
|
8435
|
+
const fromM = fm.match(/^from:\s*(.+)$/m);
|
|
8436
|
+
const toM = fm.match(/^to:\s*(.+)$/m);
|
|
8437
|
+
const typeM = fm.match(/^type:\s*(.+)$/m);
|
|
8438
|
+
const prioM = fm.match(/^priority:\s*(.+)$/m);
|
|
8439
|
+
if (fromM)
|
|
8440
|
+
from = fromM[1].trim();
|
|
8441
|
+
if (toM)
|
|
8442
|
+
to = toM[1].trim();
|
|
8443
|
+
if (typeM)
|
|
8444
|
+
type = typeM[1].trim();
|
|
8445
|
+
if (prioM)
|
|
8446
|
+
priority = prioM[1].trim();
|
|
8447
|
+
}
|
|
8448
|
+
// Body = content after the closing ---
|
|
8449
|
+
const bodyMatch = f.content.match(/^---\n[\s\S]*?\n---\n\n?([\s\S]*)$/);
|
|
8450
|
+
const body = bodyMatch ? bodyMatch[1].trim() : f.content.trim();
|
|
8451
|
+
const reqBody = {
|
|
8452
|
+
to,
|
|
8453
|
+
from,
|
|
8454
|
+
type,
|
|
8455
|
+
priority,
|
|
8456
|
+
body,
|
|
8457
|
+
no_notify: true, // backfill is historical — never fire Slack fan-out
|
|
8458
|
+
source: 'migration',
|
|
8459
|
+
};
|
|
8460
|
+
await api('POST', '/api/dispatches', reqBody);
|
|
8461
|
+
imported++;
|
|
8462
|
+
console.log(chalk.green(` ✓ ${f.relativePath}`));
|
|
8463
|
+
}
|
|
8464
|
+
catch (err) {
|
|
8465
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8466
|
+
// 409 = already imported (content hash dedup on server side)
|
|
8467
|
+
if (msg.includes('409') || msg.toLowerCase().includes('already imported')) {
|
|
8468
|
+
skipped++;
|
|
8469
|
+
console.log(chalk.gray(` ~ ${f.relativePath} (already imported)`));
|
|
8470
|
+
}
|
|
8471
|
+
else {
|
|
8472
|
+
errors.push(`${f.relativePath}: ${msg}`);
|
|
8473
|
+
console.log(chalk.yellow(` ✗ ${f.relativePath}: ${msg}`));
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
}
|
|
8477
|
+
console.log();
|
|
8478
|
+
console.log(`Backfill complete: ${chalk.green(String(imported))} imported, ${chalk.gray(String(skipped))} skipped, ${errors.length > 0 ? chalk.red(String(errors.length)) : '0'} errors`);
|
|
8479
|
+
if (errors.length > 0)
|
|
8480
|
+
process.exit(1);
|
|
8481
|
+
}
|
|
8482
|
+
catch (err) {
|
|
8483
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
8484
|
+
process.exit(1);
|
|
8485
|
+
}
|
|
8486
|
+
});
|
|
8173
8487
|
program
|
|
8174
8488
|
.command('dispatch-list-roles')
|
|
8175
8489
|
.description('List roles available in the nearest .fazemos/roles.json registry')
|