@formigio/fazemos-cli 0.6.1 → 0.7.0
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/api.d.ts +63 -2
- package/dist/api.js +107 -5
- package/dist/api.js.map +1 -1
- package/dist/config.d.ts +90 -0
- package/dist/config.js +90 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +428 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { config, getEnv, getToken, getActiveOrgId, setActiveOrgId, addEnvironment, hasEnvironments
|
|
4
|
+
import { config, getEnv, getToken, getActiveOrgId, setActiveOrgId, addEnvironment, hasEnvironments,
|
|
5
|
+
// F15 — project context helpers
|
|
6
|
+
getActiveProjectId, setActiveProjectId, clearActiveProjectId, findProjectBySlug, findProjectById, findOrgById, } from './config.js';
|
|
5
7
|
import { login, signup, confirmSignup, adminLogin } from './auth.js';
|
|
6
|
-
import { api, ApiError } from './api.js';
|
|
8
|
+
import { api, ApiError, refreshAuthMeCache, invalidateAuthMeCache } from './api.js';
|
|
7
9
|
import { execSync } from 'child_process';
|
|
8
10
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
9
11
|
import { fileURLToPath } from 'url';
|
|
@@ -249,13 +251,47 @@ auth
|
|
|
249
251
|
// ── Whoami ──────────────────────────────────────────────────
|
|
250
252
|
program
|
|
251
253
|
.command('whoami')
|
|
252
|
-
.description('Show current user and
|
|
254
|
+
.description('Show current user, org, and project context')
|
|
253
255
|
.action(async () => {
|
|
254
256
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
// Force-refresh the /auth/me cache on whoami so subsequent slug
|
|
258
|
+
// resolutions (e.g., `projects switch vahmos`) see the fresh state
|
|
259
|
+
// without a second round-trip.
|
|
260
|
+
const data = await refreshAuthMeCache();
|
|
261
|
+
if (!data) {
|
|
262
|
+
// Fall back to a direct call if refresh failed silently.
|
|
263
|
+
const direct = await api('GET', '/auth/me', undefined, { noProjectHeader: true });
|
|
264
|
+
console.log(` User: ${chalk.cyan(direct.user.email)}`);
|
|
265
|
+
console.log(` Member: ${direct.member.displayName} (${direct.member.role})`);
|
|
266
|
+
console.log(` Org: ${direct.member.orgId}`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log(` User: ${chalk.cyan(data.user.email)}`);
|
|
270
|
+
console.log(` Member: ${data.member.displayName} (${data.member.role})`);
|
|
271
|
+
const activeOrgId = getActiveOrgId() ?? data.activeOrgId;
|
|
272
|
+
const activeOrg = data.orgs.find(o => o.id === activeOrgId);
|
|
273
|
+
if (activeOrg) {
|
|
274
|
+
console.log(` Org: ${chalk.cyan(activeOrg.name)} (${activeOrg.slug})`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
console.log(` Org: ${activeOrgId}`);
|
|
278
|
+
}
|
|
279
|
+
const activeProjectId = getActiveProjectId();
|
|
280
|
+
if (activeProjectId && activeOrg) {
|
|
281
|
+
const activeProject = activeOrg.projects.find(p => p.id === activeProjectId);
|
|
282
|
+
if (activeProject) {
|
|
283
|
+
console.log(` Project: ${chalk.cyan(activeProject.name)} (${activeProject.slug})`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.log(` Project: ${activeProjectId}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (activeOrg && activeOrg.projects.length === 0) {
|
|
290
|
+
console.log(` Project: ${chalk.gray('(none — create one with: fazemos projects create <slug> --name <n>)')}`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
console.log(` Project: ${chalk.gray('(none set — fazemos projects switch <slug>)')}`);
|
|
294
|
+
}
|
|
259
295
|
}
|
|
260
296
|
catch (err) {
|
|
261
297
|
console.error(chalk.red(err.message));
|
|
@@ -269,7 +305,7 @@ orgs
|
|
|
269
305
|
.description('List your organizations')
|
|
270
306
|
.action(async () => {
|
|
271
307
|
try {
|
|
272
|
-
const data = await api('GET', '/api/organizations/mine');
|
|
308
|
+
const data = await api('GET', '/api/organizations/mine', undefined, { noProjectHeader: true });
|
|
273
309
|
if (data.organizations.length === 0) {
|
|
274
310
|
console.log(chalk.yellow('No organizations. Create one with: fazemos orgs create'));
|
|
275
311
|
return;
|
|
@@ -313,11 +349,11 @@ orgs
|
|
|
313
349
|
});
|
|
314
350
|
orgs
|
|
315
351
|
.command('switch')
|
|
316
|
-
.description('Switch active organization')
|
|
352
|
+
.description('Switch active organization. Restores the last active project for this org from config, or clears it if none has been used here before.')
|
|
317
353
|
.argument('<slug>', 'Organization slug')
|
|
318
354
|
.action(async (slug) => {
|
|
319
355
|
try {
|
|
320
|
-
const data = await api('GET', '/api/organizations/mine');
|
|
356
|
+
const data = await api('GET', '/api/organizations/mine', undefined, { noProjectHeader: true });
|
|
321
357
|
const org = data.organizations.find((o) => o.slug === slug);
|
|
322
358
|
if (!org) {
|
|
323
359
|
console.error(chalk.red(`No organization with slug "${slug}"`));
|
|
@@ -329,6 +365,36 @@ orgs
|
|
|
329
365
|
}
|
|
330
366
|
setActiveOrgId(org.id);
|
|
331
367
|
console.log(chalk.green(`Switched to ${org.name} (${org.slug})`));
|
|
368
|
+
// F15 — refresh /auth/me so the next `fazemos projects list` (and any
|
|
369
|
+
// slug→id resolutions) see the project set for the new org. Without
|
|
370
|
+
// this the cached authMeCache is effectively stale and slug lookups
|
|
371
|
+
// force an extra network call each time.
|
|
372
|
+
invalidateAuthMeCache();
|
|
373
|
+
await refreshAuthMeCache();
|
|
374
|
+
// Report the restored active project, if any. Matches UX §1.8
|
|
375
|
+
// "Switching Org" — restore last-used project for this org or fall
|
|
376
|
+
// back to the first project alphabetically.
|
|
377
|
+
const activeProjectId = getActiveProjectId();
|
|
378
|
+
if (activeProjectId) {
|
|
379
|
+
const restored = findProjectById(org.id, activeProjectId);
|
|
380
|
+
if (restored) {
|
|
381
|
+
console.log(chalk.cyan(`Restored project: ${restored.name} (${restored.slug})`));
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
// Stored project id is no longer in this org (archived?). Clear it.
|
|
385
|
+
clearActiveProjectId(org.id);
|
|
386
|
+
console.log(chalk.gray('Previously active project for this org is no longer available.'));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
const orgCache = findOrgById(org.id);
|
|
391
|
+
if (orgCache && orgCache.projects.length === 0) {
|
|
392
|
+
console.log(chalk.gray('No projects in this org yet. Create one with: fazemos projects create <slug> --name <n>'));
|
|
393
|
+
}
|
|
394
|
+
else if (orgCache && orgCache.projects.length > 0) {
|
|
395
|
+
console.log(chalk.gray(`No active project set. Run: fazemos projects switch <slug>`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
332
398
|
}
|
|
333
399
|
catch (err) {
|
|
334
400
|
console.error(chalk.red(err.message));
|
|
@@ -848,15 +914,333 @@ notifications
|
|
|
848
914
|
process.exit(1);
|
|
849
915
|
}
|
|
850
916
|
});
|
|
917
|
+
// ── F15 — Scoped-command helpers ────────────────────────────
|
|
918
|
+
// Shared helpers for commands whose API calls require a Project context.
|
|
919
|
+
// Keeps the two new flags (--project <slug>, --all-projects) and the
|
|
920
|
+
// uniform missing-project error rendering in one place so every scoped
|
|
921
|
+
// command uses the exact same shape.
|
|
922
|
+
/**
|
|
923
|
+
* Extract ApiOptions (projectSlug + allProjects) from a commander opts
|
|
924
|
+
* bag. Call this inside any `.action()` that forwards to `api()`.
|
|
925
|
+
*/
|
|
926
|
+
function projectOpts(opts) {
|
|
927
|
+
return {
|
|
928
|
+
projectSlug: opts.project,
|
|
929
|
+
allProjects: opts.allProjects,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Uniform error handler for scoped commands. When the API emits
|
|
934
|
+
* MISSING_PROJECT_CONTEXT (§7.3.1), the api helper has already re-shaped
|
|
935
|
+
* the message to "requirement missing: project". This helper prints that
|
|
936
|
+
* as the error AND appends the standard three-option hint block per KD9.
|
|
937
|
+
*
|
|
938
|
+
* All other errors pass through with their original message (preserving
|
|
939
|
+
* PROJECT_NOT_FOUND, PROJECT_MISMATCH, VALIDATION_ERROR wording) — the
|
|
940
|
+
* CLI's job is to be a thin validator, not to re-author API errors.
|
|
941
|
+
*/
|
|
942
|
+
function handleScopedError(err) {
|
|
943
|
+
if (err instanceof ApiError && err.code === 'MISSING_PROJECT_CONTEXT') {
|
|
944
|
+
console.error(chalk.red('Error: requirement missing: project'));
|
|
945
|
+
console.error('');
|
|
946
|
+
console.error(chalk.gray('Set one with: fazemos projects switch <slug>'));
|
|
947
|
+
console.error(chalk.gray('Or pass: --project <slug>'));
|
|
948
|
+
console.error(chalk.gray('Or view all: --all-projects'));
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
if (err instanceof Error) {
|
|
952
|
+
console.error(chalk.red(err.message));
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
console.error(chalk.red(String(err)));
|
|
956
|
+
}
|
|
957
|
+
process.exit(1);
|
|
958
|
+
}
|
|
959
|
+
// ── F15 — Projects ──────────────────────────────────────────
|
|
960
|
+
const projects = program.command('projects').alias('proj').description('Project commands');
|
|
961
|
+
projects
|
|
962
|
+
.command('list')
|
|
963
|
+
.description('List projects in the active organization')
|
|
964
|
+
.option('-s, --status <status>', 'Filter by status: active (default), archived, or all', 'active')
|
|
965
|
+
.action(async (opts) => {
|
|
966
|
+
try {
|
|
967
|
+
const orgId = requireActiveOrgOrExit();
|
|
968
|
+
const data = await api('GET', `/api/organizations/${orgId}/projects?status=${encodeURIComponent(opts.status)}`, undefined, { noProjectHeader: true });
|
|
969
|
+
if (!data.projects || data.projects.length === 0) {
|
|
970
|
+
if (opts.status === 'active') {
|
|
971
|
+
console.log(chalk.yellow('No active projects in this organization.'));
|
|
972
|
+
console.log(chalk.gray('Create one with: fazemos projects create <slug> --name <name>'));
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
console.log(chalk.yellow(`No ${opts.status} projects.`));
|
|
976
|
+
}
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const activeProjectId = getActiveProjectId();
|
|
980
|
+
for (const p of data.projects) {
|
|
981
|
+
const active = p.id === activeProjectId ? chalk.green(' ✓') : '';
|
|
982
|
+
const status = p.status === 'archived' ? chalk.gray(' [archived]') : '';
|
|
983
|
+
const activity = p.lastActivityAt
|
|
984
|
+
? chalk.gray(` — last active ${formatRelative(new Date(p.lastActivityAt))}`)
|
|
985
|
+
: '';
|
|
986
|
+
console.log(` ${chalk.cyan(p.name)} (${p.slug})${active}${status}${activity}`);
|
|
987
|
+
if (p.stats) {
|
|
988
|
+
const stats = [];
|
|
989
|
+
if (p.stats.activePipelines)
|
|
990
|
+
stats.push(`${p.stats.activePipelines} pipelines`);
|
|
991
|
+
if (p.stats.worksheets)
|
|
992
|
+
stats.push(`${p.stats.worksheets} worksheets`);
|
|
993
|
+
if (p.stats.templates)
|
|
994
|
+
stats.push(`${p.stats.templates} templates`);
|
|
995
|
+
if (stats.length)
|
|
996
|
+
console.log(chalk.gray(` ${stats.join(' · ')}`));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
catch (err) {
|
|
1001
|
+
console.error(chalk.red(err.message));
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
projects
|
|
1006
|
+
.command('create')
|
|
1007
|
+
.description('Create a new project in the active organization. Owner/admin only. Slug is immutable after creation — pick carefully.')
|
|
1008
|
+
.argument('<slug>', 'URL slug (lowercase letters, numbers, hyphens; 1-32 chars). Used in URLs and CLI config — cannot be changed.')
|
|
1009
|
+
.requiredOption('-n, --name <name>', 'Human-readable name (max 200 chars)')
|
|
1010
|
+
.option('-d, --description <desc>', 'Optional description (max 2000 chars)')
|
|
1011
|
+
.action(async (slug, opts) => {
|
|
1012
|
+
try {
|
|
1013
|
+
const orgId = requireActiveOrgOrExit();
|
|
1014
|
+
const body = {
|
|
1015
|
+
slug,
|
|
1016
|
+
name: opts.name,
|
|
1017
|
+
};
|
|
1018
|
+
if (opts.description)
|
|
1019
|
+
body.description = opts.description;
|
|
1020
|
+
const data = await api('POST', `/api/organizations/${orgId}/projects`, body, { noProjectHeader: true });
|
|
1021
|
+
const p = data.project;
|
|
1022
|
+
console.log(chalk.green(`Created project: ${p.name} (${p.slug})`));
|
|
1023
|
+
console.log(` ID: ${p.id}`);
|
|
1024
|
+
console.log(` Org: ${orgId}`);
|
|
1025
|
+
if (p.description)
|
|
1026
|
+
console.log(` Desc: ${p.description}`);
|
|
1027
|
+
// Invalidate cached /auth/me and set as active — a fresh project is
|
|
1028
|
+
// usually created immediately before scoped work on it, so auto-
|
|
1029
|
+
// switching saves an extra command. Matches the spec's post-create
|
|
1030
|
+
// success path (UX §2.1 toast "You're in.").
|
|
1031
|
+
invalidateAuthMeCache();
|
|
1032
|
+
setActiveProjectId(orgId, p.id);
|
|
1033
|
+
console.log(chalk.cyan(`Switched active project to ${p.slug}.`));
|
|
1034
|
+
}
|
|
1035
|
+
catch (err) {
|
|
1036
|
+
if (err instanceof ApiError && err.code === 'SLUG_CONFLICT') {
|
|
1037
|
+
console.error(chalk.red(`Error: "${slug}" is already taken in this organization.`));
|
|
1038
|
+
console.log(chalk.gray('Pick a different slug or use: fazemos projects list'));
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
console.error(chalk.red(err.message));
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
projects
|
|
1046
|
+
.command('show')
|
|
1047
|
+
.description('Show project details by slug. Resolves against the active organization.')
|
|
1048
|
+
.argument('<slug>', 'Project slug')
|
|
1049
|
+
.action(async (slug) => {
|
|
1050
|
+
try {
|
|
1051
|
+
const orgId = requireActiveOrgOrExit();
|
|
1052
|
+
// Resolve slug -> id via cached /auth/me; refresh once on miss.
|
|
1053
|
+
let project = findProjectBySlug(orgId, slug);
|
|
1054
|
+
if (!project) {
|
|
1055
|
+
await refreshAuthMeCache();
|
|
1056
|
+
project = findProjectBySlug(orgId, slug);
|
|
1057
|
+
}
|
|
1058
|
+
if (!project) {
|
|
1059
|
+
console.error(chalk.red(`Unknown project: ${slug}`));
|
|
1060
|
+
console.log(chalk.gray('Run: fazemos projects list'));
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
const data = await api('GET', `/api/organizations/${orgId}/projects/${project.id}`, undefined, { noProjectHeader: true });
|
|
1064
|
+
const p = data.project;
|
|
1065
|
+
console.log(chalk.cyan(p.name));
|
|
1066
|
+
console.log(` Slug: ${p.slug}`);
|
|
1067
|
+
console.log(` ID: ${p.id}`);
|
|
1068
|
+
console.log(` Status: ${p.status === 'archived' ? chalk.gray('archived') : chalk.green('active')}`);
|
|
1069
|
+
if (p.description)
|
|
1070
|
+
console.log(` Description: ${p.description}`);
|
|
1071
|
+
console.log(` Created: ${p.createdAt ? new Date(p.createdAt).toLocaleString() : '(unknown)'}`);
|
|
1072
|
+
if (p.archivedAt)
|
|
1073
|
+
console.log(` Archived at: ${new Date(p.archivedAt).toLocaleString()}`);
|
|
1074
|
+
if (p.stats) {
|
|
1075
|
+
console.log('');
|
|
1076
|
+
console.log(chalk.cyan('Activity:'));
|
|
1077
|
+
console.log(` Active pipelines: ${p.stats.activePipelines ?? 0}`);
|
|
1078
|
+
console.log(` Worksheets: ${p.stats.worksheets ?? 0}`);
|
|
1079
|
+
console.log(` Templates: ${p.stats.templates ?? 0}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
catch (err) {
|
|
1083
|
+
console.error(chalk.red(err.message));
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
projects
|
|
1088
|
+
.command('switch')
|
|
1089
|
+
.description('Switch active project. Persists in ~/.fazemos/config.json as the active project for the current org.')
|
|
1090
|
+
.argument('<slug>', 'Project slug')
|
|
1091
|
+
.action(async (slug) => {
|
|
1092
|
+
try {
|
|
1093
|
+
const orgId = requireActiveOrgOrExit();
|
|
1094
|
+
let project = findProjectBySlug(orgId, slug);
|
|
1095
|
+
if (!project) {
|
|
1096
|
+
// Slug-miss cache refresh per §8.3.
|
|
1097
|
+
await refreshAuthMeCache();
|
|
1098
|
+
project = findProjectBySlug(orgId, slug);
|
|
1099
|
+
}
|
|
1100
|
+
if (!project) {
|
|
1101
|
+
console.error(chalk.red(`Unknown project: ${slug}`));
|
|
1102
|
+
console.log(chalk.gray('Run: fazemos projects list'));
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
setActiveProjectId(orgId, project.id);
|
|
1106
|
+
const org = findOrgById(orgId);
|
|
1107
|
+
const orgName = org?.name ?? orgId;
|
|
1108
|
+
console.log(chalk.green(`Switched to project: ${project.name} (${project.slug}) in ${orgName}`));
|
|
1109
|
+
}
|
|
1110
|
+
catch (err) {
|
|
1111
|
+
console.error(chalk.red(err.message));
|
|
1112
|
+
process.exit(1);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
projects
|
|
1116
|
+
.command('archive')
|
|
1117
|
+
.description('Archive a project. Puts it in read-only mode; existing work is preserved. Owner/admin only. Blocked if any pipeline is currently running — wait for it to finish or cancel it first.')
|
|
1118
|
+
.argument('<slug>', 'Project slug')
|
|
1119
|
+
.action(async (slug) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const orgId = requireActiveOrgOrExit();
|
|
1122
|
+
let project = findProjectBySlug(orgId, slug);
|
|
1123
|
+
if (!project) {
|
|
1124
|
+
await refreshAuthMeCache();
|
|
1125
|
+
project = findProjectBySlug(orgId, slug);
|
|
1126
|
+
}
|
|
1127
|
+
if (!project) {
|
|
1128
|
+
console.error(chalk.red(`Unknown project: ${slug}`));
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
const data = await api('POST', `/api/organizations/${orgId}/projects/${project.id}/archive`, {}, { noProjectHeader: true });
|
|
1132
|
+
console.log(chalk.green(`Archived: ${data.project.name} (${data.project.slug})`));
|
|
1133
|
+
invalidateAuthMeCache();
|
|
1134
|
+
// If this was the active project, clear it — subsequent scoped
|
|
1135
|
+
// commands will emit "requirement missing: project" per KD9.
|
|
1136
|
+
if (getActiveProjectId() === project.id) {
|
|
1137
|
+
clearActiveProjectId(orgId);
|
|
1138
|
+
console.log(chalk.cyan('Active project cleared. Run: fazemos projects switch <slug>'));
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
catch (err) {
|
|
1142
|
+
if (err instanceof ApiError && err.code === 'PROJECT_HAS_RUNNING_PIPELINES') {
|
|
1143
|
+
console.error(chalk.red(`Can't archive ${slug} yet — pipelines are still running.`));
|
|
1144
|
+
const body = err.body;
|
|
1145
|
+
if (body?.runningPipelines?.length) {
|
|
1146
|
+
console.log('');
|
|
1147
|
+
console.log(chalk.yellow('Running pipelines:'));
|
|
1148
|
+
for (const p of body.runningPipelines) {
|
|
1149
|
+
console.log(` · ${p.name} (${p.id})`);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
console.log('');
|
|
1153
|
+
console.log(chalk.gray('Wait for them to finish or cancel them, then retry.'));
|
|
1154
|
+
process.exit(1);
|
|
1155
|
+
}
|
|
1156
|
+
console.error(chalk.red(err.message));
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
projects
|
|
1161
|
+
.command('unarchive')
|
|
1162
|
+
.description('Restore an archived project to active status. Owner/admin only.')
|
|
1163
|
+
.argument('<slug>', 'Project slug (must be archived)')
|
|
1164
|
+
.action(async (slug) => {
|
|
1165
|
+
try {
|
|
1166
|
+
const orgId = requireActiveOrgOrExit();
|
|
1167
|
+
// Archived projects aren't in the default /auth/me response, so we
|
|
1168
|
+
// can't always resolve via cache. Fall back to the full-status list
|
|
1169
|
+
// endpoint to find the id.
|
|
1170
|
+
let project = findProjectBySlug(orgId, slug);
|
|
1171
|
+
if (!project) {
|
|
1172
|
+
const listData = await api('GET', `/api/organizations/${orgId}/projects?status=all`, undefined, { noProjectHeader: true });
|
|
1173
|
+
const match = (listData.projects ?? []).find((p) => p.slug === slug);
|
|
1174
|
+
if (match) {
|
|
1175
|
+
project = {
|
|
1176
|
+
id: match.id,
|
|
1177
|
+
slug: match.slug,
|
|
1178
|
+
name: match.name,
|
|
1179
|
+
colorIndex: match.colorIndex ?? 0,
|
|
1180
|
+
status: match.status,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (!project) {
|
|
1185
|
+
console.error(chalk.red(`Unknown project: ${slug}`));
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
const data = await api('POST', `/api/organizations/${orgId}/projects/${project.id}/unarchive`, {}, { noProjectHeader: true });
|
|
1189
|
+
console.log(chalk.green(`Unarchived: ${data.project.name} (${data.project.slug})`));
|
|
1190
|
+
invalidateAuthMeCache();
|
|
1191
|
+
}
|
|
1192
|
+
catch (err) {
|
|
1193
|
+
console.error(chalk.red(err.message));
|
|
1194
|
+
process.exit(1);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
projects
|
|
1198
|
+
.command('update')
|
|
1199
|
+
.description('Update project name or description. Slug is immutable — to use a different slug, create a new project. Owner/admin only.')
|
|
1200
|
+
.argument('<slug>', 'Project slug')
|
|
1201
|
+
.option('-n, --name <name>', 'New name')
|
|
1202
|
+
.option('-d, --description <desc>', 'New description (pass empty string to clear)')
|
|
1203
|
+
.action(async (slug, opts) => {
|
|
1204
|
+
try {
|
|
1205
|
+
if (!opts.name && opts.description == null) {
|
|
1206
|
+
console.error(chalk.red('Provide --name and/or --description'));
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
const orgId = requireActiveOrgOrExit();
|
|
1210
|
+
let project = findProjectBySlug(orgId, slug);
|
|
1211
|
+
if (!project) {
|
|
1212
|
+
await refreshAuthMeCache();
|
|
1213
|
+
project = findProjectBySlug(orgId, slug);
|
|
1214
|
+
}
|
|
1215
|
+
if (!project) {
|
|
1216
|
+
console.error(chalk.red(`Unknown project: ${slug}`));
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
}
|
|
1219
|
+
const body = {};
|
|
1220
|
+
if (opts.name)
|
|
1221
|
+
body.name = opts.name;
|
|
1222
|
+
if (opts.description != null)
|
|
1223
|
+
body.description = opts.description;
|
|
1224
|
+
const data = await api('PATCH', `/api/organizations/${orgId}/projects/${project.id}`, body, { noProjectHeader: true });
|
|
1225
|
+
console.log(chalk.green(`Updated: ${data.project.name} (${data.project.slug})`));
|
|
1226
|
+
invalidateAuthMeCache();
|
|
1227
|
+
}
|
|
1228
|
+
catch (err) {
|
|
1229
|
+
console.error(chalk.red(err.message));
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
851
1233
|
// ── Worksheets ──────────────────────────────────────────────
|
|
852
1234
|
const ws = program.command('worksheets').alias('ws').description('Worksheet commands');
|
|
853
1235
|
ws
|
|
854
1236
|
.command('list')
|
|
855
|
-
.description('List worksheets')
|
|
1237
|
+
.description('List worksheets in the active project (or all projects with --all-projects)')
|
|
856
1238
|
.option('-s, --status <status>', 'Filter by status', 'active')
|
|
1239
|
+
.option('--project <slug>', 'Override active project for this call')
|
|
1240
|
+
.option('--all-projects', 'List worksheets across every project in the active org', false)
|
|
857
1241
|
.action(async (opts) => {
|
|
858
1242
|
try {
|
|
859
|
-
const data = await api('GET', `/api/worksheets?status=${opts.status}
|
|
1243
|
+
const data = await api('GET', `/api/worksheets?status=${opts.status}`, undefined, projectOpts(opts));
|
|
860
1244
|
if (data.worksheets.length === 0) {
|
|
861
1245
|
console.log(chalk.yellow('No worksheets'));
|
|
862
1246
|
return;
|
|
@@ -866,16 +1250,16 @@ ws
|
|
|
866
1250
|
}
|
|
867
1251
|
}
|
|
868
1252
|
catch (err) {
|
|
869
|
-
|
|
870
|
-
process.exit(1);
|
|
1253
|
+
handleScopedError(err);
|
|
871
1254
|
}
|
|
872
1255
|
});
|
|
873
1256
|
ws
|
|
874
1257
|
.command('create')
|
|
875
|
-
.description('Create a worksheet')
|
|
1258
|
+
.description('Create a worksheet in the active project')
|
|
876
1259
|
.requiredOption('-n, --name <name>', 'Worksheet name')
|
|
877
1260
|
.option('-p, --purpose <purpose>', 'Purpose (e.g., "From X to Y by When")')
|
|
878
1261
|
.option('--cadence <cadence>', 'Check-in cadence (weekly, biweekly, monthly)', 'weekly')
|
|
1262
|
+
.option('--project <slug>', 'Override active project for this call')
|
|
879
1263
|
.action(async (opts) => {
|
|
880
1264
|
try {
|
|
881
1265
|
const body = { name: opts.name };
|
|
@@ -883,7 +1267,7 @@ ws
|
|
|
883
1267
|
body.purpose = opts.purpose;
|
|
884
1268
|
if (opts.cadence)
|
|
885
1269
|
body.checkInCadence = opts.cadence;
|
|
886
|
-
const data = await api('POST', '/api/worksheets', body);
|
|
1270
|
+
const data = await api('POST', '/api/worksheets', body, projectOpts(opts));
|
|
887
1271
|
const w = data.worksheet;
|
|
888
1272
|
console.log(chalk.green(`Created: ${w.name}`));
|
|
889
1273
|
console.log(` ID: ${w.id}`);
|
|
@@ -891,8 +1275,7 @@ ws
|
|
|
891
1275
|
console.log(` Cadence: ${w.check_in_cadence}`);
|
|
892
1276
|
}
|
|
893
1277
|
catch (err) {
|
|
894
|
-
|
|
895
|
-
process.exit(1);
|
|
1278
|
+
handleScopedError(err);
|
|
896
1279
|
}
|
|
897
1280
|
});
|
|
898
1281
|
ws
|
|
@@ -1960,12 +2343,14 @@ const templates = program.command('templates').alias('tpl').description('Pipelin
|
|
|
1960
2343
|
' Use "tpl show <id>" to see full structure with I/O declarations.');
|
|
1961
2344
|
templates
|
|
1962
2345
|
.command('list')
|
|
1963
|
-
.description('List pipeline templates')
|
|
2346
|
+
.description('List pipeline templates in the active project (or all projects with --all-projects)')
|
|
1964
2347
|
.option('-s, --status <status>', 'Filter by status (draft, active, archived)')
|
|
2348
|
+
.option('--project <slug>', 'Override active project for this call')
|
|
2349
|
+
.option('--all-projects', 'List templates across every project in the active org', false)
|
|
1965
2350
|
.action(async (opts) => {
|
|
1966
2351
|
try {
|
|
1967
2352
|
const qs = opts.status ? `?status=${opts.status}` : '';
|
|
1968
|
-
const data = await api('GET', `/api/pipeline-templates${qs}
|
|
2353
|
+
const data = await api('GET', `/api/pipeline-templates${qs}`, undefined, projectOpts(opts));
|
|
1969
2354
|
if (!data.templates?.length) {
|
|
1970
2355
|
console.log(chalk.yellow('No templates'));
|
|
1971
2356
|
return;
|
|
@@ -1977,8 +2362,7 @@ templates
|
|
|
1977
2362
|
}
|
|
1978
2363
|
}
|
|
1979
2364
|
catch (err) {
|
|
1980
|
-
|
|
1981
|
-
process.exit(1);
|
|
2365
|
+
handleScopedError(err);
|
|
1982
2366
|
}
|
|
1983
2367
|
});
|
|
1984
2368
|
templates
|
|
@@ -3086,10 +3470,12 @@ templates
|
|
|
3086
3470
|
const pipelines = program.command('pipelines').alias('pl').description('Pipeline instance commands');
|
|
3087
3471
|
pipelines
|
|
3088
3472
|
.command('list')
|
|
3089
|
-
.description('List pipeline instances')
|
|
3473
|
+
.description('List pipeline instances in the active project (or all projects with --all-projects)')
|
|
3090
3474
|
.option('-s, --status <status>', 'Filter by status (active, completed, archived)', 'active')
|
|
3091
3475
|
.option('--search <term>', 'Search by name or ID')
|
|
3092
3476
|
.option('--expand', 'Include steps inline (avoids N+1)')
|
|
3477
|
+
.option('--project <slug>', 'Override active project for this call')
|
|
3478
|
+
.option('--all-projects', 'List pipelines across every project in the active org', false)
|
|
3093
3479
|
.action(async (opts) => {
|
|
3094
3480
|
try {
|
|
3095
3481
|
const params = [];
|
|
@@ -3100,7 +3486,7 @@ pipelines
|
|
|
3100
3486
|
if (opts.expand)
|
|
3101
3487
|
params.push('expand=steps');
|
|
3102
3488
|
const qs = params.length ? `?${params.join('&')}` : '';
|
|
3103
|
-
const data = await api('GET', `/api/pipeline-instances${qs}
|
|
3489
|
+
const data = await api('GET', `/api/pipeline-instances${qs}`, undefined, projectOpts(opts));
|
|
3104
3490
|
if (!data.instances?.length) {
|
|
3105
3491
|
console.log(chalk.yellow('No pipeline instances'));
|
|
3106
3492
|
return;
|
|
@@ -3121,8 +3507,7 @@ pipelines
|
|
|
3121
3507
|
}
|
|
3122
3508
|
}
|
|
3123
3509
|
catch (err) {
|
|
3124
|
-
|
|
3125
|
-
process.exit(1);
|
|
3510
|
+
handleScopedError(err);
|
|
3126
3511
|
}
|
|
3127
3512
|
});
|
|
3128
3513
|
pipelines
|
|
@@ -3613,10 +3998,12 @@ step
|
|
|
3613
3998
|
const queue = program.command('queue').alias('q').description('Work queue commands');
|
|
3614
3999
|
queue
|
|
3615
4000
|
.command('summary')
|
|
3616
|
-
.description('Show queue depth per agent')
|
|
3617
|
-
.
|
|
4001
|
+
.description('Show queue depth per agent, scoped to the active project (or all projects with --all-projects)')
|
|
4002
|
+
.option('--project <slug>', 'Override active project for this call')
|
|
4003
|
+
.option('--all-projects', 'Show queue across every project in the active org', false)
|
|
4004
|
+
.action(async (opts) => {
|
|
3618
4005
|
try {
|
|
3619
|
-
const data = await api('GET', '/api/work-queue/summary');
|
|
4006
|
+
const data = await api('GET', '/api/work-queue/summary', undefined, projectOpts(opts));
|
|
3620
4007
|
const entries = Object.entries(data.depths ?? {});
|
|
3621
4008
|
if (entries.length === 0) {
|
|
3622
4009
|
console.log(chalk.yellow('No agents in queue'));
|
|
@@ -3631,15 +4018,16 @@ queue
|
|
|
3631
4018
|
console.log(` ${'Total'.padEnd(12)} ${total} steps`);
|
|
3632
4019
|
}
|
|
3633
4020
|
catch (err) {
|
|
3634
|
-
|
|
3635
|
-
process.exit(1);
|
|
4021
|
+
handleScopedError(err);
|
|
3636
4022
|
}
|
|
3637
4023
|
});
|
|
3638
4024
|
queue
|
|
3639
4025
|
.command('list')
|
|
3640
|
-
.description('List queued and in-progress steps')
|
|
4026
|
+
.description('List queued and in-progress steps in the active project (or all projects with --all-projects)')
|
|
3641
4027
|
.option('-a, --agent <name>', 'Filter by agent name')
|
|
3642
4028
|
.option('--message-id <id>', 'Filter by JOE messageId in step metadata')
|
|
4029
|
+
.option('--project <slug>', 'Override active project for this call')
|
|
4030
|
+
.option('--all-projects', 'List queue items across every project in the active org', false)
|
|
3643
4031
|
.action(async (opts) => {
|
|
3644
4032
|
try {
|
|
3645
4033
|
const params = [];
|
|
@@ -3648,7 +4036,7 @@ queue
|
|
|
3648
4036
|
if (opts.messageId)
|
|
3649
4037
|
params.push(`messageId=${encodeURIComponent(opts.messageId)}`);
|
|
3650
4038
|
const qs = params.length ? `?${params.join('&')}` : '';
|
|
3651
|
-
const data = await api('GET', `/api/work-queue${qs}
|
|
4039
|
+
const data = await api('GET', `/api/work-queue${qs}`, undefined, projectOpts(opts));
|
|
3652
4040
|
if (!data.items?.length) {
|
|
3653
4041
|
console.log(chalk.yellow('No queued steps'));
|
|
3654
4042
|
return;
|
|
@@ -3660,8 +4048,7 @@ queue
|
|
|
3660
4048
|
}
|
|
3661
4049
|
}
|
|
3662
4050
|
catch (err) {
|
|
3663
|
-
|
|
3664
|
-
process.exit(1);
|
|
4051
|
+
handleScopedError(err);
|
|
3665
4052
|
}
|
|
3666
4053
|
});
|
|
3667
4054
|
// ── Executions ─────────────────────────────────────────────
|
|
@@ -4155,10 +4542,12 @@ executions
|
|
|
4155
4542
|
// ── My Work ─────────────────────────────────────────────────
|
|
4156
4543
|
program
|
|
4157
4544
|
.command('my-work')
|
|
4158
|
-
.description('Show pending work
|
|
4159
|
-
.
|
|
4545
|
+
.description('Show pending work in the active project (or all projects with --all-projects)')
|
|
4546
|
+
.option('--project <slug>', 'Override active project for this call')
|
|
4547
|
+
.option('--all-projects', 'Show pending work across every project in the active org', false)
|
|
4548
|
+
.action(async (opts) => {
|
|
4160
4549
|
try {
|
|
4161
|
-
const data = await api('GET', '/api/my-work');
|
|
4550
|
+
const data = await api('GET', '/api/my-work', undefined, projectOpts(opts));
|
|
4162
4551
|
const c = data.commitments;
|
|
4163
4552
|
const total = c.overdue.length + c.due_today.length + c.due_this_week.length + c.upcoming.length;
|
|
4164
4553
|
console.log(chalk.cyan(`Commitments: ${total}`));
|
|
@@ -4175,8 +4564,7 @@ program
|
|
|
4175
4564
|
console.log(chalk.cyan(`Pipeline steps: ${data.pipeline_steps.length}`));
|
|
4176
4565
|
}
|
|
4177
4566
|
catch (err) {
|
|
4178
|
-
|
|
4179
|
-
process.exit(1);
|
|
4567
|
+
handleScopedError(err);
|
|
4180
4568
|
}
|
|
4181
4569
|
});
|
|
4182
4570
|
// ── Test ────────────────────────────────────────────────────
|