@formigio/fazemos-cli 0.9.0 → 0.10.1
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 +556 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4298,7 +4298,7 @@ step
|
|
|
4298
4298
|
.action(async (instanceId, stepId, opts) => {
|
|
4299
4299
|
try {
|
|
4300
4300
|
const data = await api('POST', `/api/pipeline-instances/${instanceId}/steps/${stepId}/revise`, {
|
|
4301
|
-
|
|
4301
|
+
feedback: opts.reason,
|
|
4302
4302
|
});
|
|
4303
4303
|
console.log(chalk.green(`Revision requested: ${data.step?.step_name || stepId}`));
|
|
4304
4304
|
console.log(` Status: ${data.step?.status}`);
|
|
@@ -6646,5 +6646,560 @@ ops
|
|
|
6646
6646
|
process.exit(1);
|
|
6647
6647
|
}
|
|
6648
6648
|
});
|
|
6649
|
+
// ── F18 — Project-scoped docs surface ────────────────────────────────
|
|
6650
|
+
// Spec: specs/tech/platform/F18-project-scoped-docs-surface-tech-spec.md §8
|
|
6651
|
+
//
|
|
6652
|
+
// Five subcommands expose the Project docs tree + individual file reads
|
|
6653
|
+
// that F17 + F18 API ship. Active-Project-aware (F15): resolve active
|
|
6654
|
+
// Project from config; `--project <slug>` overrides. No active Project +
|
|
6655
|
+
// no override → exit 1 with the uniform "requirement missing: project"
|
|
6656
|
+
// block (matches handleScopedError + the F17 agents subcommands).
|
|
6657
|
+
//
|
|
6658
|
+
// Server-side F18 /docs/file currently returns a flat
|
|
6659
|
+
// { path, file_type, content, sha, cached_at, etag }
|
|
6660
|
+
// envelope (see fazemos-api src/services/projectDocsService.ts). The tech
|
|
6661
|
+
// spec §4.2 also describes `frontmatter`, `body`, and `source` fields on
|
|
6662
|
+
// the response — those are Phase 3 web-surface additions not present in
|
|
6663
|
+
// the Phase 2 D2 API. The CLI renders the header line from the response's
|
|
6664
|
+
// `file_type` and the composed `<docs_root>/<path>` string (since the
|
|
6665
|
+
// source.path field doesn't exist yet on the wire). This is called out in
|
|
6666
|
+
// the handoff report as an API contract gap.
|
|
6667
|
+
//
|
|
6668
|
+
// Step-doc 422 handling (Phase 2 revision R2): when the server returns
|
|
6669
|
+
// 422 with code STEP_DOC_* and a `validation_errors` array, `docs cat`
|
|
6670
|
+
// prints the errors to stderr and exits 2 (distinct from the generic
|
|
6671
|
+
// error exit 1 so scripts can branch on "malformed doc" vs other failures).
|
|
6672
|
+
const docs = program.command('docs').description('Project docs browser. Read the Project-scoped tree and individual files that F18 exposes (Project agents, steps, and general docs) without opening the web UI.');
|
|
6673
|
+
/**
|
|
6674
|
+
* F18 — resolve {orgId, projectId, projectSlug} for a docs subcommand.
|
|
6675
|
+
* Mirrors F17's `requireProjectForAgents` but also surfaces the project
|
|
6676
|
+
* slug for display purposes (header lines, GitHub URLs, local-path probe).
|
|
6677
|
+
*
|
|
6678
|
+
* On no-active-Project + no-override: emits the uniform F15 "requirement
|
|
6679
|
+
* missing: project" block and exits 1. Reuses the wording from
|
|
6680
|
+
* handleScopedError — the KD9 convention says the CLI always prints this
|
|
6681
|
+
* as a triple-hint block (switch / --project / --all-projects), but
|
|
6682
|
+
* --all-projects has no meaning for docs subcommands (each Project has
|
|
6683
|
+
* its own docs tree — "all-projects" would be N API calls with no unified
|
|
6684
|
+
* output). We omit the --all-projects hint here on purpose.
|
|
6685
|
+
*/
|
|
6686
|
+
async function requireProjectForDocs(slugOverride) {
|
|
6687
|
+
const orgId = requireActiveOrgOrExit();
|
|
6688
|
+
let projectId = null;
|
|
6689
|
+
let projectSlug = null;
|
|
6690
|
+
if (slugOverride) {
|
|
6691
|
+
let project = findProjectBySlug(orgId, slugOverride);
|
|
6692
|
+
if (!project) {
|
|
6693
|
+
await refreshAuthMeCache();
|
|
6694
|
+
project = findProjectBySlug(orgId, slugOverride);
|
|
6695
|
+
}
|
|
6696
|
+
if (!project) {
|
|
6697
|
+
console.error(chalk.red(`Unknown project: ${slugOverride}`));
|
|
6698
|
+
console.error(chalk.gray('Run: fazemos projects list'));
|
|
6699
|
+
process.exit(1);
|
|
6700
|
+
}
|
|
6701
|
+
projectId = project.id;
|
|
6702
|
+
projectSlug = project.slug;
|
|
6703
|
+
}
|
|
6704
|
+
else {
|
|
6705
|
+
projectId = getActiveProjectId();
|
|
6706
|
+
if (projectId) {
|
|
6707
|
+
const proj = findProjectById(orgId, projectId);
|
|
6708
|
+
projectSlug = proj?.slug ?? null;
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
if (!projectId) {
|
|
6712
|
+
console.error(chalk.red('Error: requirement missing: project'));
|
|
6713
|
+
console.error('');
|
|
6714
|
+
console.error(chalk.gray('Set one with: fazemos projects switch <slug>'));
|
|
6715
|
+
console.error(chalk.gray('Or pass: --project <slug>'));
|
|
6716
|
+
process.exit(1);
|
|
6717
|
+
}
|
|
6718
|
+
// projectSlug can still be null if the cache is empty and we went the
|
|
6719
|
+
// active-project path — refresh once so the smoke-test path of "I just
|
|
6720
|
+
// authed in another shell" doesn't print UUIDs in headers.
|
|
6721
|
+
if (!projectSlug) {
|
|
6722
|
+
await refreshAuthMeCache();
|
|
6723
|
+
const proj = findProjectById(orgId, projectId);
|
|
6724
|
+
projectSlug = proj?.slug ?? projectId; // fall back to UUID if still unknown
|
|
6725
|
+
}
|
|
6726
|
+
return { orgId, projectId, projectSlug };
|
|
6727
|
+
}
|
|
6728
|
+
/**
|
|
6729
|
+
* Render the `source` block as a short header label for `docs cat`.
|
|
6730
|
+
* Tech spec §4.2 discriminator:
|
|
6731
|
+
* file → "file"
|
|
6732
|
+
* file + level='org' → "file (org)"
|
|
6733
|
+
* system_config → "system config"
|
|
6734
|
+
* template → "template"
|
|
6735
|
+
* Falls back to "file" when the response pre-dates Phase 4b and omits
|
|
6736
|
+
* the `source` block entirely (defensive — the API always ships it now).
|
|
6737
|
+
*/
|
|
6738
|
+
function renderOriginLabel(source) {
|
|
6739
|
+
if (!source)
|
|
6740
|
+
return 'file';
|
|
6741
|
+
if (source.type === 'system_config')
|
|
6742
|
+
return 'system config';
|
|
6743
|
+
if (source.type === 'template')
|
|
6744
|
+
return 'template';
|
|
6745
|
+
if (source.level === 'org' || source.level === 'org_db')
|
|
6746
|
+
return 'file (org)';
|
|
6747
|
+
return 'file';
|
|
6748
|
+
}
|
|
6749
|
+
/**
|
|
6750
|
+
* Fetch a Project's docs tree. Returns the raw payload. Swallows nothing —
|
|
6751
|
+
* callers that want empty-state friendly output should branch on
|
|
6752
|
+
* `payload.empty` + `payload.reason`.
|
|
6753
|
+
*/
|
|
6754
|
+
async function fetchDocsTree(orgId, projectId) {
|
|
6755
|
+
return (await api('GET', `/api/organizations/${orgId}/projects/${projectId}/docs/tree`, undefined, { noProjectHeader: true }));
|
|
6756
|
+
}
|
|
6757
|
+
/**
|
|
6758
|
+
* Render a flat list of tree entries as an indented text tree. The API
|
|
6759
|
+
* returns paths with embedded slashes ("docs/product/fazemos-concept.md")
|
|
6760
|
+
* and entries for both files and directories. We build a directory map
|
|
6761
|
+
* from the entries and walk it alphabetically for stable output.
|
|
6762
|
+
*
|
|
6763
|
+
* Output format matches tech spec §8.2 — directory names end with `/`,
|
|
6764
|
+
* files get 2-space indent per nesting level, and a file-type badge is
|
|
6765
|
+
* appended for `agent` / `step` files (not `doc` — those are the default
|
|
6766
|
+
* and badging every markdown file would be noisy). Size is NOT printed
|
|
6767
|
+
* (noisy at list scale). Directory file-counts are computed locally.
|
|
6768
|
+
*/
|
|
6769
|
+
function renderDocsTree(entries) {
|
|
6770
|
+
const root = { name: '', fullPath: '', isDir: true, children: new Map() };
|
|
6771
|
+
function insert(path, entry, forceDir = false) {
|
|
6772
|
+
const parts = path.split('/');
|
|
6773
|
+
let cur = root;
|
|
6774
|
+
for (let i = 0; i < parts.length; i++) {
|
|
6775
|
+
const part = parts[i];
|
|
6776
|
+
const isLast = i === parts.length - 1;
|
|
6777
|
+
const isDir = !isLast || forceDir || entry?.type === 'dir';
|
|
6778
|
+
let child = cur.children.get(part);
|
|
6779
|
+
if (!child) {
|
|
6780
|
+
child = {
|
|
6781
|
+
name: part,
|
|
6782
|
+
fullPath: parts.slice(0, i + 1).join('/'),
|
|
6783
|
+
isDir,
|
|
6784
|
+
children: new Map(),
|
|
6785
|
+
};
|
|
6786
|
+
cur.children.set(part, child);
|
|
6787
|
+
}
|
|
6788
|
+
if (isLast) {
|
|
6789
|
+
child.entry = entry;
|
|
6790
|
+
// Once marked as dir (by its own entry), don't downgrade.
|
|
6791
|
+
if (isDir)
|
|
6792
|
+
child.isDir = true;
|
|
6793
|
+
}
|
|
6794
|
+
cur = child;
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
for (const e of entries) {
|
|
6798
|
+
insert(e.path, e, e.type === 'dir');
|
|
6799
|
+
}
|
|
6800
|
+
function countFiles(node) {
|
|
6801
|
+
if (!node.isDir)
|
|
6802
|
+
return 1;
|
|
6803
|
+
let n = 0;
|
|
6804
|
+
for (const c of node.children.values())
|
|
6805
|
+
n += countFiles(c);
|
|
6806
|
+
return n;
|
|
6807
|
+
}
|
|
6808
|
+
const lines = [];
|
|
6809
|
+
function walk(node, depth) {
|
|
6810
|
+
const kids = Array.from(node.children.values()).sort((a, b) => {
|
|
6811
|
+
// Directories first, then files, both alphabetical.
|
|
6812
|
+
if (a.isDir !== b.isDir)
|
|
6813
|
+
return a.isDir ? -1 : 1;
|
|
6814
|
+
return a.name.localeCompare(b.name);
|
|
6815
|
+
});
|
|
6816
|
+
for (const c of kids) {
|
|
6817
|
+
const indent = ' '.repeat(depth);
|
|
6818
|
+
if (c.isDir) {
|
|
6819
|
+
const count = countFiles(c);
|
|
6820
|
+
const tag = count ? chalk.gray(` (${count} ${count === 1 ? 'file' : 'files'})`) : '';
|
|
6821
|
+
lines.push(`${indent}${c.name}/${tag}`);
|
|
6822
|
+
walk(c, depth + 1);
|
|
6823
|
+
}
|
|
6824
|
+
else {
|
|
6825
|
+
const ft = c.entry?.file_type;
|
|
6826
|
+
const badge = ft === 'agent'
|
|
6827
|
+
? chalk.cyan(' [agent]')
|
|
6828
|
+
: ft === 'step'
|
|
6829
|
+
? chalk.magenta(' [step]')
|
|
6830
|
+
: '';
|
|
6831
|
+
lines.push(`${indent}${c.name}${badge}`);
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
walk(root, 0);
|
|
6836
|
+
return lines;
|
|
6837
|
+
}
|
|
6838
|
+
/**
|
|
6839
|
+
* Format the "Synced N ago" sub-header from a cached_at ISO string. Keep
|
|
6840
|
+
* wording identical to UX §5.1 example ("Synced just now") when <30s.
|
|
6841
|
+
*/
|
|
6842
|
+
function formatSyncedLine(cachedAtIso) {
|
|
6843
|
+
try {
|
|
6844
|
+
const cached = new Date(cachedAtIso).getTime();
|
|
6845
|
+
const now = Date.now();
|
|
6846
|
+
const seconds = Math.max(0, Math.floor((now - cached) / 1000));
|
|
6847
|
+
if (seconds < 30)
|
|
6848
|
+
return 'Synced just now';
|
|
6849
|
+
if (seconds < 90)
|
|
6850
|
+
return 'Synced 1 minute ago';
|
|
6851
|
+
if (seconds < 60 * 60)
|
|
6852
|
+
return `Synced ${Math.floor(seconds / 60)} minutes ago`;
|
|
6853
|
+
if (seconds < 2 * 60 * 60)
|
|
6854
|
+
return 'Synced 1 hour ago';
|
|
6855
|
+
return `Synced ${Math.floor(seconds / 3600)} hours ago`;
|
|
6856
|
+
}
|
|
6857
|
+
catch {
|
|
6858
|
+
return `Synced at ${cachedAtIso}`;
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
// ── docs tree ────────────────────────────────────────────────────────
|
|
6862
|
+
docs
|
|
6863
|
+
.command('tree')
|
|
6864
|
+
.description('Show the Project\'s docs tree (agents/, steps/, docs/, plus the Project CLAUDE.md). Output is stable text suitable for piping.')
|
|
6865
|
+
.option('--include-shared', 'Also print Org-shared docs (under `docs/` and root CLAUDE.md) as a second section', false)
|
|
6866
|
+
.option('--project <slug>', 'Override active Project for this call')
|
|
6867
|
+
.action(async (opts) => {
|
|
6868
|
+
try {
|
|
6869
|
+
const { orgId, projectId, projectSlug } = await requireProjectForDocs(opts.project);
|
|
6870
|
+
const payload = await fetchDocsTree(orgId, projectId);
|
|
6871
|
+
const repoBadge = payload.repo ? `${payload.repo} at ${payload.root ?? ''}` : '(no repo configured)';
|
|
6872
|
+
console.log(chalk.cyan(`Docs — ${projectSlug} (${repoBadge})`));
|
|
6873
|
+
console.log(chalk.gray(formatSyncedLine(payload.cached_at)));
|
|
6874
|
+
console.log('');
|
|
6875
|
+
if (payload.empty) {
|
|
6876
|
+
if (payload.reason === 'no_docs_repo') {
|
|
6877
|
+
console.log(chalk.yellow('No docs repository configured.'));
|
|
6878
|
+
console.log(chalk.gray(' Set one with: fazemos projects update <slug> --docs-repo <owner/name> --docs-root <path>'));
|
|
6879
|
+
}
|
|
6880
|
+
else if (payload.reason === 'no_connection') {
|
|
6881
|
+
console.log(chalk.yellow('Project has no active GitHub Connection.'));
|
|
6882
|
+
console.log(chalk.gray(' Set one up with: fazemos connections ...'));
|
|
6883
|
+
}
|
|
6884
|
+
else {
|
|
6885
|
+
console.log(chalk.yellow('Docs tree is empty.'));
|
|
6886
|
+
}
|
|
6887
|
+
return;
|
|
6888
|
+
}
|
|
6889
|
+
for (const line of renderDocsTree(payload.tree))
|
|
6890
|
+
console.log(line);
|
|
6891
|
+
if (opts.includeShared) {
|
|
6892
|
+
console.log('');
|
|
6893
|
+
console.log(chalk.gray('── Org shared ─────────────────────────────────────────'));
|
|
6894
|
+
if (payload.org_shared && payload.org_shared.length) {
|
|
6895
|
+
for (const line of renderDocsTree(payload.org_shared))
|
|
6896
|
+
console.log(line);
|
|
6897
|
+
}
|
|
6898
|
+
else {
|
|
6899
|
+
// Phase 2 API does not yet populate org_shared (see the file-level
|
|
6900
|
+
// note at the top of this block). Tell the operator what we see.
|
|
6901
|
+
console.log(chalk.gray('(no org-shared docs returned by the API — Phase 2 ships Project-only)'));
|
|
6902
|
+
}
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
catch (err) {
|
|
6906
|
+
handleScopedError(err);
|
|
6907
|
+
}
|
|
6908
|
+
});
|
|
6909
|
+
// ── docs cat ─────────────────────────────────────────────────────────
|
|
6910
|
+
/**
|
|
6911
|
+
* F18 §8 — step-doc validation failure exit code. Distinct from the
|
|
6912
|
+
* generic error exit 1 so operator scripts can branch on "file is
|
|
6913
|
+
* structurally invalid" vs "server 5xx / auth / other".
|
|
6914
|
+
*/
|
|
6915
|
+
const DOCS_STEP_VALIDATION_EXIT = 2;
|
|
6916
|
+
docs
|
|
6917
|
+
.command('cat')
|
|
6918
|
+
.description('Print a Project doc to stdout. Adds a one-line header showing the file type and source path. On malformed step docs, prints validation errors to stderr and exits 2.')
|
|
6919
|
+
.argument('<path>', 'Relative path under docs_root (e.g., agents/marco.md, steps/feature-pipeline/tech-spec.md)')
|
|
6920
|
+
.option('--project <slug>', 'Override active Project for this call')
|
|
6921
|
+
.action(async (relPath, opts) => {
|
|
6922
|
+
try {
|
|
6923
|
+
const { orgId, projectId } = await requireProjectForDocs(opts.project);
|
|
6924
|
+
// Phase 4b: the API now ships `source.path` on every /docs/file
|
|
6925
|
+
// response (tech spec §4.2), so the Phase-3b tree pre-fetch that
|
|
6926
|
+
// compensated for the missing field is gone. `source.path` is the
|
|
6927
|
+
// authoritative repo-relative path. `tree` is only needed for the
|
|
6928
|
+
// Org-shared fallback below (kept for resilience — if the server
|
|
6929
|
+
// ever omits `source`, this still degrades to a helpful header).
|
|
6930
|
+
let payload;
|
|
6931
|
+
try {
|
|
6932
|
+
payload = (await api('GET', `/api/organizations/${orgId}/projects/${projectId}/docs/file?path=${encodeURIComponent(relPath)}`, undefined, { noProjectHeader: true }));
|
|
6933
|
+
}
|
|
6934
|
+
catch (err) {
|
|
6935
|
+
// Phase 2 revision R2: 422 on malformed step docs. The API
|
|
6936
|
+
// returns `{ error, code: STEP_DOC_*, details: { path, validation_errors: [...] } }`.
|
|
6937
|
+
if (err instanceof ApiError &&
|
|
6938
|
+
err.status === 422 &&
|
|
6939
|
+
typeof err.code === 'string' &&
|
|
6940
|
+
err.code.startsWith('STEP_DOC_')) {
|
|
6941
|
+
const body = (err.body ?? {});
|
|
6942
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
6943
|
+
console.error(chalk.red(` Code: ${err.code}`));
|
|
6944
|
+
const ve = body.details?.validation_errors ?? [];
|
|
6945
|
+
if (ve.length) {
|
|
6946
|
+
console.error(chalk.red(' Validation errors:'));
|
|
6947
|
+
for (const v of ve) {
|
|
6948
|
+
const field = v.field ? `${v.field}: ` : '';
|
|
6949
|
+
console.error(chalk.red(` - ${field}${v.message ?? v.code ?? 'unknown'}`));
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
process.exit(DOCS_STEP_VALIDATION_EXIT);
|
|
6953
|
+
}
|
|
6954
|
+
throw err;
|
|
6955
|
+
}
|
|
6956
|
+
// Header: file type + source discriminator + repo-relative path.
|
|
6957
|
+
// Phase 4b: `source.path` / `source.type` / `source.level` are
|
|
6958
|
+
// always present on a 200. The header reflects the §4.2 source
|
|
6959
|
+
// block so an operator piping `docs cat` to a reviewer sees
|
|
6960
|
+
// exactly where the bytes came from (file vs. system_config vs.
|
|
6961
|
+
// template-inline).
|
|
6962
|
+
const sourcePath = payload.source?.path ?? payload.path;
|
|
6963
|
+
const typeLabel = payload.file_type === 'agent'
|
|
6964
|
+
? chalk.cyan('agent')
|
|
6965
|
+
: payload.file_type === 'step'
|
|
6966
|
+
? chalk.magenta('step')
|
|
6967
|
+
: chalk.gray('doc');
|
|
6968
|
+
const originLabel = renderOriginLabel(payload.source);
|
|
6969
|
+
console.log(chalk.gray(`# ${typeLabel} ${chalk.gray('·')} ${originLabel}: ${sourcePath}`));
|
|
6970
|
+
console.log('');
|
|
6971
|
+
if (payload.content == null) {
|
|
6972
|
+
// DB-sourced response (system_config / template) per tech spec §4.2.
|
|
6973
|
+
// content is null; the callable body is in `body` (Phase 3+).
|
|
6974
|
+
if (payload.body) {
|
|
6975
|
+
console.log(payload.body);
|
|
6976
|
+
}
|
|
6977
|
+
else {
|
|
6978
|
+
console.log(chalk.gray('(no file content — this doc is sourced from system config / template inline; body field not yet exposed)'));
|
|
6979
|
+
}
|
|
6980
|
+
return;
|
|
6981
|
+
}
|
|
6982
|
+
// Trim trailing newlines; re-emit exactly one so `| grep` output
|
|
6983
|
+
// doesn't end with multiple blank lines.
|
|
6984
|
+
process.stdout.write(payload.content);
|
|
6985
|
+
if (!payload.content.endsWith('\n'))
|
|
6986
|
+
process.stdout.write('\n');
|
|
6987
|
+
}
|
|
6988
|
+
catch (err) {
|
|
6989
|
+
handleScopedError(err);
|
|
6990
|
+
}
|
|
6991
|
+
});
|
|
6992
|
+
docs
|
|
6993
|
+
.command('search')
|
|
6994
|
+
.description('Search the Project docs tree by filename/path (case-insensitive). Phase 3: path-match only; server-side full-text search is deferred per spec §8.6.')
|
|
6995
|
+
.argument('<query>', 'Substring to search for in file paths')
|
|
6996
|
+
.option('--scope <scope>', 'Scope: this-project (default) | agents | steps | all', 'this-project')
|
|
6997
|
+
.option('--project <slug>', 'Override active Project for this call')
|
|
6998
|
+
.action(async (query, opts) => {
|
|
6999
|
+
const scope = (opts.scope ?? 'this-project');
|
|
7000
|
+
if (!['this-project', 'agents', 'steps', 'all'].includes(scope)) {
|
|
7001
|
+
console.error(chalk.red(`Invalid --scope "${opts.scope}". Allowed: this-project, agents, steps, all`));
|
|
7002
|
+
process.exit(1);
|
|
7003
|
+
}
|
|
7004
|
+
try {
|
|
7005
|
+
const { orgId, projectId } = await requireProjectForDocs(opts.project);
|
|
7006
|
+
const payload = await fetchDocsTree(orgId, projectId);
|
|
7007
|
+
const pool = [];
|
|
7008
|
+
for (const e of payload.tree)
|
|
7009
|
+
if (e.type === 'file')
|
|
7010
|
+
pool.push(e);
|
|
7011
|
+
if (scope === 'all') {
|
|
7012
|
+
for (const e of payload.org_shared ?? [])
|
|
7013
|
+
if (e.type === 'file')
|
|
7014
|
+
pool.push(e);
|
|
7015
|
+
}
|
|
7016
|
+
const filtered = pool.filter((e) => {
|
|
7017
|
+
if (scope === 'agents')
|
|
7018
|
+
return e.file_type === 'agent';
|
|
7019
|
+
if (scope === 'steps')
|
|
7020
|
+
return e.file_type === 'step';
|
|
7021
|
+
return true; // this-project | all
|
|
7022
|
+
});
|
|
7023
|
+
const q = query.toLowerCase();
|
|
7024
|
+
const matches = filtered.filter((e) => e.path.toLowerCase().includes(q));
|
|
7025
|
+
if (!matches.length) {
|
|
7026
|
+
console.log(chalk.yellow(`No matches for "${query}" in scope "${scope}".`));
|
|
7027
|
+
return;
|
|
7028
|
+
}
|
|
7029
|
+
// Highlight the matched substring. chalk.yellow the match so it's
|
|
7030
|
+
// visible even in monochrome terminals (dim doesn't show well on
|
|
7031
|
+
// light backgrounds).
|
|
7032
|
+
for (const e of matches) {
|
|
7033
|
+
const low = e.path.toLowerCase();
|
|
7034
|
+
let out = '';
|
|
7035
|
+
let i = 0;
|
|
7036
|
+
while (i < e.path.length) {
|
|
7037
|
+
const at = low.indexOf(q, i);
|
|
7038
|
+
if (at < 0) {
|
|
7039
|
+
out += e.path.slice(i);
|
|
7040
|
+
break;
|
|
7041
|
+
}
|
|
7042
|
+
out += e.path.slice(i, at);
|
|
7043
|
+
out += chalk.yellow(e.path.slice(at, at + q.length));
|
|
7044
|
+
i = at + q.length;
|
|
7045
|
+
}
|
|
7046
|
+
const badge = e.file_type === 'agent'
|
|
7047
|
+
? chalk.cyan(' [agent]')
|
|
7048
|
+
: e.file_type === 'step'
|
|
7049
|
+
? chalk.magenta(' [step]')
|
|
7050
|
+
: '';
|
|
7051
|
+
console.log(` ${out}${badge}`);
|
|
7052
|
+
}
|
|
7053
|
+
console.log(chalk.gray(`\n${matches.length} ${matches.length === 1 ? 'match' : 'matches'} in scope "${scope}".`));
|
|
7054
|
+
}
|
|
7055
|
+
catch (err) {
|
|
7056
|
+
handleScopedError(err);
|
|
7057
|
+
}
|
|
7058
|
+
});
|
|
7059
|
+
// ── docs path ────────────────────────────────────────────────────────
|
|
7060
|
+
/**
|
|
7061
|
+
* F18 §8.5 — print the local workspace clone path for this Project,
|
|
7062
|
+
* falling back to the GitHub URL when no local clone is detected.
|
|
7063
|
+
*
|
|
7064
|
+
* Local detection heuristic:
|
|
7065
|
+
* 1. $FAZEMOS_WORKSPACE_PATH env var, if set + dir exists.
|
|
7066
|
+
* 2. `~/development/<repo-name>/` (the Fazemos convention) + dir exists.
|
|
7067
|
+
* If either matches, the full path is `<workspace>/<docs_root>`.
|
|
7068
|
+
*
|
|
7069
|
+
* When neither matches, we print the GitHub tree URL
|
|
7070
|
+
* (https://github.com/<owner>/<repo>/tree/<branch>/<root>) so `open $(fazemos docs path)`
|
|
7071
|
+
* on a fresh machine gracefully routes to the web UI instead of failing.
|
|
7072
|
+
*/
|
|
7073
|
+
docs
|
|
7074
|
+
.command('path')
|
|
7075
|
+
.description('Print the local filesystem path to this Project\'s docs root, or the GitHub URL if no local workspace clone is detected. Useful for `cd $(fazemos docs path)`.')
|
|
7076
|
+
.option('--project <slug>', 'Override active Project for this call')
|
|
7077
|
+
.action(async (opts) => {
|
|
7078
|
+
try {
|
|
7079
|
+
const { orgId, projectId } = await requireProjectForDocs(opts.project);
|
|
7080
|
+
const payload = await fetchDocsTree(orgId, projectId);
|
|
7081
|
+
if (!payload.repo) {
|
|
7082
|
+
console.error(chalk.red('Project has no docs repository configured.'));
|
|
7083
|
+
process.exit(1);
|
|
7084
|
+
}
|
|
7085
|
+
const repo = payload.repo;
|
|
7086
|
+
const root = payload.root ?? '';
|
|
7087
|
+
const branch = payload.branch ?? 'main';
|
|
7088
|
+
// Local probe.
|
|
7089
|
+
const candidates = [];
|
|
7090
|
+
if (process.env.FAZEMOS_WORKSPACE_PATH)
|
|
7091
|
+
candidates.push(process.env.FAZEMOS_WORKSPACE_PATH);
|
|
7092
|
+
const home = process.env.HOME;
|
|
7093
|
+
const repoName = repo.split('/').pop() ?? '';
|
|
7094
|
+
if (home && repoName)
|
|
7095
|
+
candidates.push(`${home}/development/${repoName}`);
|
|
7096
|
+
for (const c of candidates) {
|
|
7097
|
+
try {
|
|
7098
|
+
if (existsSync(c) && statSync(c).isDirectory()) {
|
|
7099
|
+
// Emit to stdout only — the whole point is that `cd $(fazemos docs path)` works.
|
|
7100
|
+
console.log(root ? `${c}/${root}` : c);
|
|
7101
|
+
return;
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
catch {
|
|
7105
|
+
// ignore
|
|
7106
|
+
}
|
|
7107
|
+
}
|
|
7108
|
+
// No local clone — print the GitHub URL.
|
|
7109
|
+
const url = root
|
|
7110
|
+
? `https://github.com/${repo}/tree/${branch}/${root}`
|
|
7111
|
+
: `https://github.com/${repo}/tree/${branch}`;
|
|
7112
|
+
console.log(url);
|
|
7113
|
+
}
|
|
7114
|
+
catch (err) {
|
|
7115
|
+
handleScopedError(err);
|
|
7116
|
+
}
|
|
7117
|
+
});
|
|
7118
|
+
// ── docs open ────────────────────────────────────────────────────────
|
|
7119
|
+
/**
|
|
7120
|
+
* F18 §8 — open a doc. If the workspace repo is cloned locally AND we're
|
|
7121
|
+
* currently inside that clone (heuristic: cwd's git-root has a remote
|
|
7122
|
+
* matching the Project's configured docs_repo), launch $EDITOR. Otherwise
|
|
7123
|
+
* print the GitHub blob URL so operators on a fresh machine get a sensible
|
|
7124
|
+
* fallback.
|
|
7125
|
+
*
|
|
7126
|
+
* Why the "cwd is the git clone" heuristic rather than just "any clone
|
|
7127
|
+
* exists anywhere on disk"? Operators routinely run the CLI from inside
|
|
7128
|
+
* workspace shells (that's where the Fazemos CLI lives in the workflow)
|
|
7129
|
+
* and expect `fazemos docs open` to touch the same clone they're editing.
|
|
7130
|
+
* If they're in a different shell, opening GitHub in a browser is the
|
|
7131
|
+
* safer action than editing a clone they didn't ask about.
|
|
7132
|
+
*/
|
|
7133
|
+
docs
|
|
7134
|
+
.command('open')
|
|
7135
|
+
.description('Open a Project doc. If you\'re inside the workspace repo clone, launches $EDITOR on the file; otherwise prints the GitHub blob URL.')
|
|
7136
|
+
.argument('<path>', 'Relative path under docs_root (e.g., agents/marco.md)')
|
|
7137
|
+
.option('--project <slug>', 'Override active Project for this call')
|
|
7138
|
+
.action(async (relPath, opts) => {
|
|
7139
|
+
try {
|
|
7140
|
+
const { orgId, projectId } = await requireProjectForDocs(opts.project);
|
|
7141
|
+
const payload = await fetchDocsTree(orgId, projectId);
|
|
7142
|
+
if (!payload.repo) {
|
|
7143
|
+
console.error(chalk.red('Project has no docs repository configured.'));
|
|
7144
|
+
process.exit(1);
|
|
7145
|
+
}
|
|
7146
|
+
const repo = payload.repo;
|
|
7147
|
+
const root = payload.root ?? '';
|
|
7148
|
+
const branch = payload.branch ?? 'main';
|
|
7149
|
+
// Heuristic: are we inside a git clone whose origin matches `repo`?
|
|
7150
|
+
let localWorkspacePath = null;
|
|
7151
|
+
try {
|
|
7152
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', {
|
|
7153
|
+
encoding: 'utf-8',
|
|
7154
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
7155
|
+
}).trim();
|
|
7156
|
+
if (gitRoot) {
|
|
7157
|
+
const originUrl = execSync('git config --get remote.origin.url', {
|
|
7158
|
+
encoding: 'utf-8',
|
|
7159
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
7160
|
+
cwd: gitRoot,
|
|
7161
|
+
}).trim();
|
|
7162
|
+
// Accept any URL form that ends with the repo's `owner/name`
|
|
7163
|
+
// (with optional `.git` suffix). Covers https, ssh, and git://.
|
|
7164
|
+
const re = new RegExp(`[:/]${repo.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}(?:\\.git)?$`);
|
|
7165
|
+
if (re.test(originUrl)) {
|
|
7166
|
+
localWorkspacePath = gitRoot;
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
}
|
|
7170
|
+
catch {
|
|
7171
|
+
// Not a git repo, or git not available — fall through to URL.
|
|
7172
|
+
}
|
|
7173
|
+
if (localWorkspacePath) {
|
|
7174
|
+
const fullPath = root
|
|
7175
|
+
? `${localWorkspacePath}/${root}/${relPath}`
|
|
7176
|
+
: `${localWorkspacePath}/${relPath}`;
|
|
7177
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
7178
|
+
if (!editor) {
|
|
7179
|
+
console.error(chalk.red('No $EDITOR or $VISUAL configured. Set one (e.g., `export EDITOR=code`) or re-run from outside the workspace clone to get the GitHub URL.'));
|
|
7180
|
+
// Still print the full local path so the operator can open it manually.
|
|
7181
|
+
console.log(fullPath);
|
|
7182
|
+
process.exit(1);
|
|
7183
|
+
}
|
|
7184
|
+
try {
|
|
7185
|
+
execSync(`${editor} "${fullPath}"`, { stdio: 'inherit' });
|
|
7186
|
+
}
|
|
7187
|
+
catch (err) {
|
|
7188
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7189
|
+
console.error(chalk.red(`Failed to launch editor: ${msg}`));
|
|
7190
|
+
process.exit(1);
|
|
7191
|
+
}
|
|
7192
|
+
return;
|
|
7193
|
+
}
|
|
7194
|
+
// Not inside the workspace clone — print the GitHub blob URL.
|
|
7195
|
+
const url = root
|
|
7196
|
+
? `https://github.com/${repo}/blob/${branch}/${root}/${relPath}`
|
|
7197
|
+
: `https://github.com/${repo}/blob/${branch}/${relPath}`;
|
|
7198
|
+
console.log(url);
|
|
7199
|
+
}
|
|
7200
|
+
catch (err) {
|
|
7201
|
+
handleScopedError(err);
|
|
7202
|
+
}
|
|
7203
|
+
});
|
|
6649
7204
|
program.parse();
|
|
6650
7205
|
//# sourceMappingURL=index.js.map
|