@formigio/fazemos-cli 0.10.15 → 0.10.16

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 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';
@@ -8052,6 +8053,151 @@ Examples:
8052
8053
  if (summary.errors > 0)
8053
8054
  process.exit(1);
8054
8055
  });
8056
+ // ── `fazemos dispatch` — role-to-role dispatch ─────────────────────
8057
+ //
8058
+ // Writes an inbox markdown file in the recipient role's operating dir and
8059
+ // calls POST /api/notifications/dispatch so the API can fire any
8060
+ // configured human-filler notifications (Slack today).
8061
+ //
8062
+ // Recipients are resolved via `.fazemos/roles.json` walked up from cwd.
8063
+ // Cross-workspace roles are followed via the local registry's
8064
+ // `cross_workspace_roles` block.
8065
+ //
8066
+ // Example:
8067
+ // fazemos dispatch founder question --from business-strategist \
8068
+ // --body "Should we ship F19 before F7?" --priority high --commit
8069
+ program
8070
+ .command('dispatch <to> <type>')
8071
+ .description(`Write an inbox markdown file in <to>'s operating dir and (optionally) fire
8072
+ notifications via the API.
8073
+
8074
+ Recipients are resolved via the nearest .fazemos/roles.json registry, walking
8075
+ up from the current directory. Cross-workspace recipients are followed via
8076
+ the local registry's cross_workspace_roles block.
8077
+
8078
+ Types: question | task | signal | response | flag | decision | direction`)
8079
+ .requiredOption('--from <role>', 'sender role-slug (required)')
8080
+ .option('--body <text>', 'markdown body of the dispatch (or use --body-file)')
8081
+ .option('--body-file <path>', 'read body from a file')
8082
+ .option('--priority <level>', 'low | normal | high', 'normal')
8083
+ .option('--re <ref>', 'optional reference (worksheet id, file path, etc.)')
8084
+ .option('--thread <id>', 'optional prior item id this responds to')
8085
+ .option('--expires-at <iso>', 'optional deadline (ISO 8601)')
8086
+ .option('--commit', 'git add + commit the inbox file after writing')
8087
+ .option('--no-notify', 'skip the API notification call (file-only)')
8088
+ .action(async (to, type, opts) => {
8089
+ try {
8090
+ // Validate type
8091
+ const allowedTypes = ['question', 'task', 'signal', 'response', 'flag', 'decision', 'direction'];
8092
+ if (!allowedTypes.includes(type)) {
8093
+ throw new Error(`Invalid type "${type}". Allowed: ${allowedTypes.join(', ')}`);
8094
+ }
8095
+ // Resolve body
8096
+ let body = opts.body;
8097
+ if (!body && opts.bodyFile) {
8098
+ body = readFileSync(opts.bodyFile, 'utf-8').trim();
8099
+ }
8100
+ if (!body) {
8101
+ throw new Error('--body or --body-file is required');
8102
+ }
8103
+ // Find local registry
8104
+ const localRegistry = findLocalRegistry(process.cwd());
8105
+ if (!localRegistry) {
8106
+ throw new Error(`No .fazemos/roles.json found in cwd or any parent. ` +
8107
+ `Run from inside a Fazemos-aware workspace.`);
8108
+ }
8109
+ // Resolve recipient
8110
+ const resolved = resolveRole(to, localRegistry);
8111
+ if (!resolved) {
8112
+ throw new Error(`Role "${to}" not found in local registry or any cross-workspace ref. ` +
8113
+ `Check .fazemos/roles.json.`);
8114
+ }
8115
+ const { role, registry } = resolved;
8116
+ const input = {
8117
+ to,
8118
+ from: opts.from,
8119
+ type: type,
8120
+ priority: (opts.priority ?? 'normal'),
8121
+ body,
8122
+ re: opts.re,
8123
+ thread: opts.thread,
8124
+ expiresAt: opts.expiresAt,
8125
+ };
8126
+ // Build + write file
8127
+ const { filename, content, summary } = buildInboxFile(input);
8128
+ const fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
8129
+ const relPath = fullPath.startsWith(registry._workspaceRoot)
8130
+ ? fullPath.slice(registry._workspaceRoot.length + 1)
8131
+ : fullPath;
8132
+ console.log(chalk.green(`✓ Wrote inbox file:`));
8133
+ console.log(` ${chalk.cyan(fullPath)}`);
8134
+ console.log(` Workspace: ${registry.workspace} Filler: ${role.filler.identity} (${role.filler.type})`);
8135
+ // Notify (unless --no-notify)
8136
+ if (opts.notify !== false) {
8137
+ try {
8138
+ const payload = buildNotificationPayload(input, role, relPath, summary);
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
+ }
8146
+ }
8147
+ catch (err) {
8148
+ const msg = err instanceof Error ? err.message : String(err);
8149
+ console.log(chalk.yellow(` warning: notification call failed — ${msg}`));
8150
+ console.log(chalk.yellow(` (inbox file was still written successfully)`));
8151
+ }
8152
+ }
8153
+ else {
8154
+ console.log(chalk.gray(' (--no-notify; API not called)'));
8155
+ }
8156
+ // Commit if requested
8157
+ if (opts.commit) {
8158
+ try {
8159
+ gitCommitInboxFile(registry._workspaceRoot, relPath, `dispatch(${opts.from} → ${to}): ${type}`);
8160
+ console.log(chalk.green('✓ Committed.'));
8161
+ }
8162
+ catch (err) {
8163
+ const msg = err instanceof Error ? err.message : String(err);
8164
+ console.log(chalk.yellow(` warning: commit failed — ${msg}`));
8165
+ }
8166
+ }
8167
+ }
8168
+ catch (err) {
8169
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
8170
+ process.exit(1);
8171
+ }
8172
+ });
8173
+ program
8174
+ .command('dispatch-list-roles')
8175
+ .description('List roles available in the nearest .fazemos/roles.json registry')
8176
+ .action(() => {
8177
+ const reg = findLocalRegistry(process.cwd());
8178
+ if (!reg) {
8179
+ console.error(chalk.red('No .fazemos/roles.json found in cwd or any parent.'));
8180
+ process.exit(1);
8181
+ }
8182
+ console.log(chalk.cyan(`Registry: ${reg._registryPath}`));
8183
+ console.log(chalk.cyan(`Workspace: ${reg.workspace} Org: ${reg.org}${reg.project ? ' Project: ' + reg.project : ''}`));
8184
+ console.log();
8185
+ console.log(chalk.bold('Local roles:'));
8186
+ for (const [slug, role] of Object.entries(reg.roles)) {
8187
+ const fillerTag = role.filler.type === 'human'
8188
+ ? chalk.yellow(`${role.filler.identity} (human)`)
8189
+ : chalk.gray(`${role.filler.identity} (agent)`);
8190
+ const notifyTag = role.notification ? chalk.green(' [notify]') : '';
8191
+ console.log(` ${chalk.cyan(slug.padEnd(32))} ${fillerTag}${notifyTag}`);
8192
+ }
8193
+ if (reg.cross_workspace_roles) {
8194
+ console.log();
8195
+ console.log(chalk.bold('Cross-workspace roles:'));
8196
+ for (const [slug, xref] of Object.entries(reg.cross_workspace_roles)) {
8197
+ console.log(` ${chalk.cyan(slug.padEnd(32))} → ${xref.workspace_path}`);
8198
+ }
8199
+ }
8200
+ });
8055
8201
  // Skip auto-parse only when running under Vitest (which sets process.env.VITEST).
8056
8202
  // Tests import `program` and drive it via `program.parseAsync(...)` after mocking
8057
8203
  // `./api.js`. In every other context — direct invocation, npx tsx, OR the bin