@formigio/fazemos-cli 0.10.15 → 0.10.17
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/index.js +265 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +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, buildNotificationPayload, gitCommitInboxFile, } from './dispatch.js';
|
|
13
14
|
import { execSync } from 'child_process';
|
|
14
15
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
15
16
|
import { fileURLToPath } from 'url';
|
|
@@ -5538,14 +5539,57 @@ executions
|
|
|
5538
5539
|
// ── My Work ─────────────────────────────────────────────────
|
|
5539
5540
|
program
|
|
5540
5541
|
.command('my-work')
|
|
5541
|
-
.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)')
|
|
5542
5543
|
.option('--project <slug>', 'Override active project for this call')
|
|
5543
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)
|
|
5544
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) ─────────────────
|
|
5545
5585
|
try {
|
|
5546
5586
|
const data = await api('GET', '/api/my-work', undefined, projectOpts(opts));
|
|
5547
5587
|
const c = data.commitments;
|
|
5548
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
|
+
}
|
|
5549
5593
|
console.log(chalk.cyan(`Commitments: ${total}`));
|
|
5550
5594
|
if (c.overdue.length)
|
|
5551
5595
|
console.log(chalk.red(` Overdue: ${c.overdue.length}`));
|
|
@@ -5563,6 +5607,81 @@ program
|
|
|
5563
5607
|
handleScopedError(err);
|
|
5564
5608
|
}
|
|
5565
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
|
+
}
|
|
5566
5685
|
// ── Test ────────────────────────────────────────────────────
|
|
5567
5686
|
program
|
|
5568
5687
|
.command('test')
|
|
@@ -8052,6 +8171,151 @@ Examples:
|
|
|
8052
8171
|
if (summary.errors > 0)
|
|
8053
8172
|
process.exit(1);
|
|
8054
8173
|
});
|
|
8174
|
+
// ── `fazemos dispatch` — role-to-role dispatch ─────────────────────
|
|
8175
|
+
//
|
|
8176
|
+
// Writes an inbox markdown file in the recipient role's operating dir and
|
|
8177
|
+
// calls POST /api/notifications/dispatch so the API can fire any
|
|
8178
|
+
// configured human-filler notifications (Slack today).
|
|
8179
|
+
//
|
|
8180
|
+
// Recipients are resolved via `.fazemos/roles.json` walked up from cwd.
|
|
8181
|
+
// Cross-workspace roles are followed via the local registry's
|
|
8182
|
+
// `cross_workspace_roles` block.
|
|
8183
|
+
//
|
|
8184
|
+
// Example:
|
|
8185
|
+
// fazemos dispatch founder question --from business-strategist \
|
|
8186
|
+
// --body "Should we ship F19 before F7?" --priority high --commit
|
|
8187
|
+
program
|
|
8188
|
+
.command('dispatch <to> <type>')
|
|
8189
|
+
.description(`Write an inbox markdown file in <to>'s operating dir and (optionally) fire
|
|
8190
|
+
notifications via the API.
|
|
8191
|
+
|
|
8192
|
+
Recipients are resolved via the nearest .fazemos/roles.json registry, walking
|
|
8193
|
+
up from the current directory. Cross-workspace recipients are followed via
|
|
8194
|
+
the local registry's cross_workspace_roles block.
|
|
8195
|
+
|
|
8196
|
+
Types: question | task | signal | response | flag | decision | direction`)
|
|
8197
|
+
.requiredOption('--from <role>', 'sender role-slug (required)')
|
|
8198
|
+
.option('--body <text>', 'markdown body of the dispatch (or use --body-file)')
|
|
8199
|
+
.option('--body-file <path>', 'read body from a file')
|
|
8200
|
+
.option('--priority <level>', 'low | normal | high', 'normal')
|
|
8201
|
+
.option('--re <ref>', 'optional reference (worksheet id, file path, etc.)')
|
|
8202
|
+
.option('--thread <id>', 'optional prior item id this responds to')
|
|
8203
|
+
.option('--expires-at <iso>', 'optional deadline (ISO 8601)')
|
|
8204
|
+
.option('--commit', 'git add + commit the inbox file after writing')
|
|
8205
|
+
.option('--no-notify', 'skip the API notification call (file-only)')
|
|
8206
|
+
.action(async (to, type, opts) => {
|
|
8207
|
+
try {
|
|
8208
|
+
// Validate type
|
|
8209
|
+
const allowedTypes = ['question', 'task', 'signal', 'response', 'flag', 'decision', 'direction'];
|
|
8210
|
+
if (!allowedTypes.includes(type)) {
|
|
8211
|
+
throw new Error(`Invalid type "${type}". Allowed: ${allowedTypes.join(', ')}`);
|
|
8212
|
+
}
|
|
8213
|
+
// Resolve body
|
|
8214
|
+
let body = opts.body;
|
|
8215
|
+
if (!body && opts.bodyFile) {
|
|
8216
|
+
body = readFileSync(opts.bodyFile, 'utf-8').trim();
|
|
8217
|
+
}
|
|
8218
|
+
if (!body) {
|
|
8219
|
+
throw new Error('--body or --body-file is required');
|
|
8220
|
+
}
|
|
8221
|
+
// Find local registry
|
|
8222
|
+
const localRegistry = findLocalRegistry(process.cwd());
|
|
8223
|
+
if (!localRegistry) {
|
|
8224
|
+
throw new Error(`No .fazemos/roles.json found in cwd or any parent. ` +
|
|
8225
|
+
`Run from inside a Fazemos-aware workspace.`);
|
|
8226
|
+
}
|
|
8227
|
+
// Resolve recipient
|
|
8228
|
+
const resolved = resolveRole(to, localRegistry);
|
|
8229
|
+
if (!resolved) {
|
|
8230
|
+
throw new Error(`Role "${to}" not found in local registry or any cross-workspace ref. ` +
|
|
8231
|
+
`Check .fazemos/roles.json.`);
|
|
8232
|
+
}
|
|
8233
|
+
const { role, registry } = resolved;
|
|
8234
|
+
const input = {
|
|
8235
|
+
to,
|
|
8236
|
+
from: opts.from,
|
|
8237
|
+
type: type,
|
|
8238
|
+
priority: (opts.priority ?? 'normal'),
|
|
8239
|
+
body,
|
|
8240
|
+
re: opts.re,
|
|
8241
|
+
thread: opts.thread,
|
|
8242
|
+
expiresAt: opts.expiresAt,
|
|
8243
|
+
};
|
|
8244
|
+
// Build + write file
|
|
8245
|
+
const { filename, content, summary } = buildInboxFile(input);
|
|
8246
|
+
const fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
|
|
8247
|
+
const relPath = fullPath.startsWith(registry._workspaceRoot)
|
|
8248
|
+
? fullPath.slice(registry._workspaceRoot.length + 1)
|
|
8249
|
+
: fullPath;
|
|
8250
|
+
console.log(chalk.green(`✓ Wrote inbox file:`));
|
|
8251
|
+
console.log(` ${chalk.cyan(fullPath)}`);
|
|
8252
|
+
console.log(` Workspace: ${registry.workspace} Filler: ${role.filler.identity} (${role.filler.type})`);
|
|
8253
|
+
// Notify (unless --no-notify)
|
|
8254
|
+
if (opts.notify !== false) {
|
|
8255
|
+
try {
|
|
8256
|
+
const payload = buildNotificationPayload(input, role, relPath, summary);
|
|
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
|
+
}
|
|
8264
|
+
}
|
|
8265
|
+
catch (err) {
|
|
8266
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8267
|
+
console.log(chalk.yellow(` warning: notification call failed — ${msg}`));
|
|
8268
|
+
console.log(chalk.yellow(` (inbox file was still written successfully)`));
|
|
8269
|
+
}
|
|
8270
|
+
}
|
|
8271
|
+
else {
|
|
8272
|
+
console.log(chalk.gray(' (--no-notify; API not called)'));
|
|
8273
|
+
}
|
|
8274
|
+
// Commit if requested
|
|
8275
|
+
if (opts.commit) {
|
|
8276
|
+
try {
|
|
8277
|
+
gitCommitInboxFile(registry._workspaceRoot, relPath, `dispatch(${opts.from} → ${to}): ${type}`);
|
|
8278
|
+
console.log(chalk.green('✓ Committed.'));
|
|
8279
|
+
}
|
|
8280
|
+
catch (err) {
|
|
8281
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8282
|
+
console.log(chalk.yellow(` warning: commit failed — ${msg}`));
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
catch (err) {
|
|
8287
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
8288
|
+
process.exit(1);
|
|
8289
|
+
}
|
|
8290
|
+
});
|
|
8291
|
+
program
|
|
8292
|
+
.command('dispatch-list-roles')
|
|
8293
|
+
.description('List roles available in the nearest .fazemos/roles.json registry')
|
|
8294
|
+
.action(() => {
|
|
8295
|
+
const reg = findLocalRegistry(process.cwd());
|
|
8296
|
+
if (!reg) {
|
|
8297
|
+
console.error(chalk.red('No .fazemos/roles.json found in cwd or any parent.'));
|
|
8298
|
+
process.exit(1);
|
|
8299
|
+
}
|
|
8300
|
+
console.log(chalk.cyan(`Registry: ${reg._registryPath}`));
|
|
8301
|
+
console.log(chalk.cyan(`Workspace: ${reg.workspace} Org: ${reg.org}${reg.project ? ' Project: ' + reg.project : ''}`));
|
|
8302
|
+
console.log();
|
|
8303
|
+
console.log(chalk.bold('Local roles:'));
|
|
8304
|
+
for (const [slug, role] of Object.entries(reg.roles)) {
|
|
8305
|
+
const fillerTag = role.filler.type === 'human'
|
|
8306
|
+
? chalk.yellow(`${role.filler.identity} (human)`)
|
|
8307
|
+
: chalk.gray(`${role.filler.identity} (agent)`);
|
|
8308
|
+
const notifyTag = role.notification ? chalk.green(' [notify]') : '';
|
|
8309
|
+
console.log(` ${chalk.cyan(slug.padEnd(32))} ${fillerTag}${notifyTag}`);
|
|
8310
|
+
}
|
|
8311
|
+
if (reg.cross_workspace_roles) {
|
|
8312
|
+
console.log();
|
|
8313
|
+
console.log(chalk.bold('Cross-workspace roles:'));
|
|
8314
|
+
for (const [slug, xref] of Object.entries(reg.cross_workspace_roles)) {
|
|
8315
|
+
console.log(` ${chalk.cyan(slug.padEnd(32))} → ${xref.workspace_path}`);
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
});
|
|
8055
8319
|
// Skip auto-parse only when running under Vitest (which sets process.env.VITEST).
|
|
8056
8320
|
// Tests import `program` and drive it via `program.parseAsync(...)` after mocking
|
|
8057
8321
|
// `./api.js`. In every other context — direct invocation, npx tsx, OR the bin
|