@tuanhv/op-cli 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hoangvantuan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE CHANGED
@@ -11,5 +11,5 @@ OpenProject in the documentation and command output describe
11
11
  interoperability with the OpenProject REST API only; they are nominative
12
12
  references to that product, not claims of association.
13
13
 
14
- op-cli is released under the MIT license; see the "license" field of
15
- package.json.
14
+ op-cli is released under the MIT license; see the LICENSE file. The
15
+ "license" field of package.json carries the same grant.
package/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
  > GmbH; this tool only speaks the REST API of the OpenProject application you
8
8
  > already run. See the NOTICE file for the full statement.
9
9
  >
10
- > **Early release.** This is version 0.1.0 of a young tool. The command
11
- > surface may still change between releases; pin your version if that
12
- > matters to you.
10
+ > **Early release.** This is an early 0.x release of a young tool. The
11
+ > command surface may still change between releases; pin your version
12
+ > if that matters to you.
13
13
 
14
14
  A resolving CLI for OpenProject: every flag accepts a human name or an id,
15
15
  and the tool turns names into ids itself. Errors carry stable `[CODE]`
@@ -22,6 +22,8 @@ markers so both humans and agents can react to them programmatically.
22
22
 
23
23
  ## From install to first useful command
24
24
 
25
+ The npm package is [@tuanhv/op-cli](https://www.npmjs.com/package/@tuanhv/op-cli):
26
+
25
27
  ```sh
26
28
  npm install -g @tuanhv/op-cli
27
29
  op-cli auth login # prompts for URL and API key; needs an interactive terminal
@@ -37,10 +39,14 @@ points beyond the quickstart:
37
39
 
38
40
  ```sh
39
41
  op-cli wp create "Fix login bug" --type Bug --priority High
42
+ op-cli wp list --search login # server-side subject search
40
43
  op-cli time log 42 --hours 1h30m --activity Development
44
+ op-cli time report --from 2026-08-01 --to 2026-08-31 # closed period
41
45
  op-cli project member add web alice Manager # join members so assignment works
42
46
  op-cli project list --search web
47
+ op-cli wp move 42 archive # move a work package to another project
43
48
  op-cli doctor # diagnose connectivity, credentials, versions
49
+ op-cli update # self-update; prints the right command for Volta/Homebrew
44
50
  ```
45
51
 
46
52
  ## Things worth knowing
@@ -52,6 +58,13 @@ op-cli doctor # diagnose connectivity, credentials, versions
52
58
  default project, scopes `wp list`, `wp count`, `time list`, and
53
59
  `time report` to that project and its subprojects. Without one, those
54
60
  four commands report instance-wide.
61
+ - **Finding things needs no id.** `wp list --search <text>` matches
62
+ subjects server-side, `--created-after` / `--updated-after` bound by
63
+ date, and `time list` / `time report` take `--from` and an inclusive
64
+ `--to`. Every name flag also accepts the human name or the id.
65
+ - **Tables respect the terminal.** Read from a terminal, a table is
66
+ bounded to its width and long cells are cut with an ellipsis; `--json`
67
+ output is never truncated.
55
68
  - **Deletion is guarded.** Deleting a project is irreversible: `wp delete`,
56
69
  `time delete`, and `project delete` all require an explicit `--yes`.
57
70
  `user delete` is not offered at all; there is no workaround by design.
@@ -65,8 +78,10 @@ op-cli doctor # diagnose connectivity, credentials, versions
65
78
  when `OPENPROJECT_URL` / `OPENPROJECT_API_KEY` are exported; without the
66
79
  flag the environment wins over the active profile. A command served by the
67
80
  environment reports itself as the profile `env`.
68
- - **`project copy` copies properties only** (description, visibility, and
69
- parent). It does not copy work packages, members, or wiki pages.
81
+ - **Update notices are interactive-only.** Commands run from a terminal
82
+ check npm for a newer release at most once a day and print one stderr
83
+ line; scripted use (stderr not a TTY) never checks, `--version` stays
84
+ offline, and `OP_CLI_NO_UPDATE_CHECK=1` disables the check entirely.
70
85
  - **No escape hatch by design.** If a command is missing there is no raw
71
86
  HTTP passthrough; report the gap instead of working around the tool.
72
87
 
@@ -94,7 +109,8 @@ mappings; do not build on it.
94
109
  npm run build # compile src/ to dist/
95
110
  npm test # unit tests over fixtures from a real instance
96
111
  npm run lint
97
- scripts/smoke.sh # REQUIRED gate before publishing
112
+ scripts/smoke.sh # REQUIRED gate before publishing
113
+ npm publish --access public # scoped package; public is not the default
98
114
  ```
99
115
 
100
116
  `scripts/smoke.sh` exercises a real create, update, time log, and delete
@@ -103,6 +119,11 @@ lifecycle against a scratch project on a live instance configured through
103
119
  broken step. There are no automated integration tests by design; this
104
120
  script is the regression net for real write paths before every publish.
105
121
 
106
- ## License
122
+ Releases after the first can ship from CI: once the package exists and its
123
+ npm Trusted Publishers entry points at `.github/workflows/release.yml`,
124
+ pushing a `v*` tag has GitHub Actions publish with a short-lived OIDC token
125
+ and provenance; no npm token is stored anywhere.
107
126
 
108
- MIT. See the NOTICE file for the trademark statement.
127
+ ## License
128
+ MIT; see the LICENSE file. See the NOTICE file for the trademark
129
+ statement.
package/dist/bin.js CHANGED
@@ -1,6 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import { createNodeIo } from "./io/node-io.js";
3
3
  import { run } from "./run.js";
4
+ /** 128 + 13: the exit status a shell reports for a process killed by SIGPIPE. */
5
+ const EXIT_SIGPIPE = 141;
6
+ /**
7
+ * Node ignores SIGPIPE, so writing after a pipe consumer is gone (`op-cli
8
+ * wp list | head`) surfaces as an EPIPE stream error. Without a listener
9
+ * that error escalates to an uncaught exception with a stack trace; exit
10
+ * quietly instead, and let every other write error keep crashing as before.
11
+ */
12
+ function exitOnBrokenPipe(stream) {
13
+ stream.once("error", (error) => {
14
+ if (error.code === "EPIPE") {
15
+ process.exit(EXIT_SIGPIPE);
16
+ }
17
+ throw error;
18
+ });
19
+ }
20
+ exitOnBrokenPipe(process.stdout);
21
+ exitOnBrokenPipe(process.stderr);
4
22
  const result = await run(process.argv.slice(2), process.env, createNodeIo());
5
23
  process.stdout.write(result.stdout);
6
24
  process.stderr.write(result.stderr);
@@ -25,9 +25,12 @@ function errorCodeOf(error) {
25
25
  }
26
26
  function versionSummary(info) {
27
27
  if (info.apiVersion === null && info.coreVersion === null) {
28
- return "api unknown, core unknown (stock instances may omit this)";
28
+ return "reported neither version; the root document is optional, so core 13+ may still be running";
29
29
  }
30
- return `api ${info.apiVersion ?? "unknown"}, core ${info.coreVersion ?? "unknown"}`;
30
+ if (info.apiVersion === null) {
31
+ return `core ${info.coreVersion ?? "unknown"}; api version not reported, core 13+ is what the check reads`;
32
+ }
33
+ return `api ${info.apiVersion}, core ${info.coreVersion ?? "unknown"}`;
31
34
  }
32
35
  export function registerDoctorCommand(parent, runtime) {
33
36
  parent
@@ -1,18 +1,23 @@
1
1
  import { clearStoredMetadata, customFieldAllowedNames, loadProjectVocabulary, loadStoredMetadata, readStoredMetadata, refreshStoredMetadata, } from "../context/metadata.js";
2
- import { parseOptionalId } from "../context/profile.js";
2
+ import { parseProjectOverride } from "../context/profile.js";
3
3
  import { defineLookupCommand, } from "../core/define.js";
4
4
  import { renderTable } from "../output/table.js";
5
5
  function star(value) {
6
6
  return value === true ? "*" : "";
7
7
  }
8
8
  const projectOption = {
9
- flag: "--project <id>",
9
+ flag: "--project <name-or-id>",
10
10
  description: "override the profile default project",
11
11
  };
12
+ const profileOption = {
13
+ flag: "--profile <name>",
14
+ description: "use this profile for this command only",
15
+ };
12
16
  const typeLookup = {
13
17
  name: "types",
14
18
  description: "List the work package types of the instance",
15
19
  select: (metadata) => metadata.types,
20
+ options: [profileOption],
16
21
  columns: [
17
22
  { title: "ID", cell: (entry) => String(entry.id) },
18
23
  { title: "NAME", cell: (entry) => entry.name },
@@ -23,6 +28,7 @@ const statusLookup = {
23
28
  name: "statuses",
24
29
  description: "List the work package statuses of the instance",
25
30
  select: (metadata) => metadata.statuses,
31
+ options: [profileOption],
26
32
  columns: [
27
33
  { title: "ID", cell: (entry) => String(entry.id) },
28
34
  { title: "NAME", cell: (entry) => entry.name },
@@ -46,6 +52,7 @@ const priorityLookup = {
46
52
  name: "priorities",
47
53
  description: "List the work package priorities of the instance",
48
54
  select: (metadata) => metadata.priorities,
55
+ options: [profileOption],
49
56
  columns: [
50
57
  { title: "ID", cell: (entry) => String(entry.id) },
51
58
  { title: "NAME", cell: (entry) => entry.name },
@@ -56,7 +63,7 @@ const memberLookup = {
56
63
  name: "members",
57
64
  description: "List the members of the active project",
58
65
  select: (vocabulary) => vocabulary.members,
59
- options: [projectOption],
66
+ options: [profileOption, projectOption],
60
67
  columns: [
61
68
  { title: "ID", cell: (entry) => String(entry.user_id) },
62
69
  { title: "NAME", cell: (entry) => entry.name },
@@ -71,7 +78,7 @@ const versionLookup = {
71
78
  name: "versions",
72
79
  description: "List the versions of the active project",
73
80
  select: (vocabulary) => vocabulary.versions,
74
- options: [projectOption],
81
+ options: [profileOption, projectOption],
75
82
  columns: [
76
83
  { title: "ID", cell: (entry) => String(entry.id) },
77
84
  { title: "NAME", cell: (entry) => entry.name },
@@ -82,7 +89,7 @@ const categoryLookup = {
82
89
  name: "categories",
83
90
  description: "List the categories of the active project",
84
91
  select: (vocabulary) => vocabulary.categories,
85
- options: [projectOption],
92
+ options: [profileOption, projectOption],
86
93
  columns: [
87
94
  { title: "ID", cell: (entry) => String(entry.id) },
88
95
  { title: "NAME", cell: (entry) => entry.name },
@@ -122,7 +129,7 @@ const fieldLookup = {
122
129
  name: "fields",
123
130
  description: "List the custom fields of the active project",
124
131
  select: uniqueFields,
125
- options: [projectOption],
132
+ options: [profileOption, projectOption],
126
133
  columns: [
127
134
  { title: "ID", cell: (entry) => String(entry.id) },
128
135
  { title: "NAME", cell: (entry) => entry.name },
@@ -138,7 +145,7 @@ const activityLookup = {
138
145
  name: "activities",
139
146
  description: "List the time entry activities of the active project",
140
147
  select: (vocabulary) => vocabulary.activities,
141
- options: [projectOption],
148
+ options: [profileOption, projectOption],
142
149
  columns: [
143
150
  { title: "ID", cell: (entry) => String(entry.id) },
144
151
  { title: "NAME", cell: (entry) => entry.name },
@@ -149,10 +156,11 @@ function registerShow(parent, runtime) {
149
156
  const command = parent
150
157
  .command("show")
151
158
  .description("Show the stored metadata of the active profile")
159
+ .option("--profile <name>", "use this profile for this command only")
152
160
  .option("--json", "emit a JSON object");
153
161
  command.action(async (options) => {
154
162
  runtime.setJsonMode(options.json === true);
155
- const profile = await runtime.resolve();
163
+ const profile = await runtime.resolve({ profile: options.profile });
156
164
  const stored = await readStoredMetadata(runtime.env, profile);
157
165
  if (stored === undefined) {
158
166
  runtime.write(options.json === true
@@ -186,10 +194,11 @@ function registerRefresh(parent, runtime) {
186
194
  const command = parent
187
195
  .command("refresh")
188
196
  .description("Re-fetch the stored metadata and report what changed")
197
+ .option("--profile <name>", "use this profile for this command only")
189
198
  .option("--json", "emit a JSON object");
190
199
  command.action(async (options) => {
191
200
  runtime.setJsonMode(options.json === true);
192
- const profile = await runtime.resolve();
201
+ const profile = await runtime.resolve({ profile: options.profile });
193
202
  const changes = await refreshStoredMetadata(runtime.env, profile);
194
203
  if (options.json === true) {
195
204
  runtime.write(`${JSON.stringify({
@@ -223,8 +232,9 @@ function registerClear(parent, runtime) {
223
232
  parent
224
233
  .command("clear")
225
234
  .description("Delete the stored metadata of the active profile")
226
- .action(async () => {
227
- const profile = await runtime.resolve();
235
+ .option("--profile <name>", "use this profile for this command only")
236
+ .action(async (options) => {
237
+ const profile = await runtime.resolve({ profile: options.profile });
228
238
  await clearStoredMetadata(runtime.env, profile);
229
239
  runtime.write(`Cleared stored metadata for profile ${profile.name}.\n`);
230
240
  });
@@ -236,14 +246,19 @@ function addLookup(parent, runtime, spec, load) {
236
246
  setJsonMode: runtime.setJsonMode,
237
247
  }));
238
248
  }
249
+ /** The `--profile` override a lookup was handed, if any. */
250
+ function profileOverride(options) {
251
+ return typeof options.profile === "string" ? options.profile : undefined;
252
+ }
239
253
  function instanceLoad(runtime) {
240
- return async () => loadStoredMetadata(runtime.env, await runtime.resolve());
254
+ return async (options) => loadStoredMetadata(runtime.env, await runtime.resolve({ profile: profileOverride(options) }));
241
255
  }
242
- /** The active profile under this run's --project override, if any. */
256
+ /** The active profile under this run's --profile/--project overrides. */
243
257
  async function projectProfile(runtime, options) {
244
258
  const raw = options.project;
245
259
  return runtime.resolve({
246
- project: parseOptionalId(typeof raw === "string" ? raw : undefined),
260
+ profile: profileOverride(options),
261
+ project: parseProjectOverride(typeof raw === "string" ? raw : undefined),
247
262
  });
248
263
  }
249
264
  function projectLoad(runtime) {
@@ -6,7 +6,8 @@ import { flattenHalRecord, toFormattable } from "../core/hal.js";
6
6
  import { apiDelete, apiGet, apiPatchRaw, apiPost, apiPostRaw, authenticate, } from "../core/http.js";
7
7
  import { DEFAULT_PAGE_SIZE, allPageSize, halElements, parsePageSize, withPageSize, } from "../core/paginate.js";
8
8
  import { isIdForm, rankByCloseness, resolveName, } from "../context/resolve.js";
9
- import { parseOptionalId, } from "../context/profile.js";
9
+ import { parseProjectOverride, } from "../context/profile.js";
10
+ import { PROJECTS_COLLECTION, resolveProjectRef } from "../context/projectref.js";
10
11
  export const LIST_COLUMNS = [
11
12
  { title: "ID", field: "id" },
12
13
  { title: "IDENTIFIER", field: "identifier" },
@@ -33,7 +34,7 @@ async function connect(runtime, options) {
33
34
  runtime.setJsonMode(options.json === true);
34
35
  const profile = await runtime.resolve({
35
36
  profile: options.profile,
36
- project: parseOptionalId(options.project),
37
+ project: parseProjectOverride(options.project),
37
38
  });
38
39
  return {
39
40
  getPage: (path) => apiGet(profile.instanceUrl, profile.apiKey, path),
@@ -42,75 +43,9 @@ async function connect(runtime, options) {
42
43
  function collectValue(value, previous) {
43
44
  return [...previous, value];
44
45
  }
45
- function hitOf(element) {
46
- const candidate = element;
47
- return typeof candidate.id === "number"
48
- && typeof candidate.identifier === "string"
49
- && typeof candidate.name === "string"
50
- ? { id: candidate.id, identifier: candidate.identifier, name: candidate.name }
51
- : undefined;
52
- }
53
- const PROJECTS_COLLECTION = "/api/v3/projects";
54
46
  const PRINCIPALS_COLLECTION = "/api/v3/principals";
55
47
  const ROLES_COLLECTION = "/api/v3/roles";
56
48
  const MEMBERSHIPS_COLLECTION = "/api/v3/memberships";
57
- /**
58
- * Every visible project, page by page in server order. Resolution walks
59
- * instead of filtering server-side because the exact-match filter set
60
- * differs between instance versions; the collection endpoint does not.
61
- */
62
- async function walkProjects(getPage) {
63
- const hits = [];
64
- for await (const element of halElements(getPage, withPageSize(PROJECTS_COLLECTION, DEFAULT_PAGE_SIZE))) {
65
- const hit = hitOf(element);
66
- if (hit !== undefined) {
67
- hits.push(hit);
68
- }
69
- }
70
- return hits;
71
- }
72
- function missingProjectError(raw, known) {
73
- return new OpCliError("NOT_FOUND", `project "${raw}" not found.`, known.length > 0
74
- ? `known projects, closest first: ${rankByCloseness(raw, known).slice(0, 6).join(", ")}.`
75
- : "check the spelling or list projects with op-cli project list.");
76
- }
77
- /**
78
- * Resolve one reference that may be an id, an identifier, or a name.
79
- * All-digits means an id and is fetched directly. Anything else matches
80
- * identifier and name exactly across every visible project; one distinct
81
- * match wins, several distinct projects fail loudly instead of guessing.
82
- */
83
- async function resolveProjectRef(getPage, raw) {
84
- if (isIdForm(raw)) {
85
- try {
86
- const hit = hitOf(await getPage(`${PROJECTS_COLLECTION}/${raw}`));
87
- if (hit !== undefined) {
88
- return hit;
89
- }
90
- throw new OpCliError("NOT_FOUND", `project "${raw}" not found.`, "check the id; run op-cli project list to see what is visible.");
91
- }
92
- catch (error) {
93
- if (error instanceof OpCliError && error.code === "USAGE_ERROR") {
94
- throw error;
95
- }
96
- if (error instanceof OpCliError && error.code === "NOT_FOUND") {
97
- throw new OpCliError("NOT_FOUND", `project "${raw}" not found.`, "check the id; run op-cli project list to see what is visible.");
98
- }
99
- throw error;
100
- }
101
- }
102
- const all = await walkProjects(getPage);
103
- const matches = all.filter((hit) => hit.identifier === raw || hit.name === raw);
104
- const distinct = [...new Map(matches.map((hit) => [hit.id, hit])).values()];
105
- if (distinct.length === 1) {
106
- return distinct[0];
107
- }
108
- if (distinct.length > 1) {
109
- const listed = distinct.map((hit) => `${String(hit.id)} (${hit.name})`).join(", ");
110
- throw new OpCliError("USAGE_ERROR", `project "${raw}" is ambiguous. Candidates: ${listed}.`, "repeat the value as the explicit id of one candidate.");
111
- }
112
- throw missingProjectError(raw, all.flatMap((hit) => [hit.identifier, hit.name]));
113
- }
114
49
  /**
115
50
  * One live lookup over an uncached collection. Stored metadata has a
116
51
  * refresh to re-read the disk store; here refresh simply walks again.
@@ -276,7 +211,7 @@ export function registerProjectCommands(project, runtime) {
276
211
  .option("--limit <n>", "maximum number of results to show")
277
212
  .option("--all", "fetch every page instead of one limited page")
278
213
  .option("--profile <name>", "use this profile for this command only")
279
- .option("--project <id>", "override the profile default project")
214
+ .option("--project <name-or-id>", "override the profile default project")
280
215
  .action(async (options) => {
281
216
  runtime.setJsonMode(options.json === true);
282
217
  // Flag misuse is refused before any traffic.
@@ -352,7 +287,7 @@ export function registerProjectCommands(project, runtime) {
352
287
  .option("--json", "emit a flat JSON record")
353
288
  .option("--fields <list>", "comma-separated fields to show")
354
289
  .option("--profile <name>", "use this profile for this command only")
355
- .option("--project <id>", "override the profile default project")
290
+ .option("--project <name-or-id>", "override the profile default project")
356
291
  .action(async (reference, options) => {
357
292
  const { getPage } = await connect(runtime, options);
358
293
  const hit = await resolveProjectRef(getPage, reference);
@@ -372,13 +307,13 @@ export function registerProjectCommands(project, runtime) {
372
307
  .option("--json", "emit a flat JSON record")
373
308
  .option("--fields <list>", "comma-separated fields to show")
374
309
  .option("--profile <name>", "use this profile for this command only")
375
- .option("--project <id>", "override the profile default project")
310
+ .option("--project <name-or-id>", "override the profile default project")
376
311
  .action(async (name, options) => {
377
312
  const identifier = requireIdentifier(options, "create");
378
313
  const { getPage } = await connect(runtime, options);
379
314
  const profile = await runtime.resolve({
380
315
  profile: options.profile,
381
- project: parseOptionalId(options.project),
316
+ project: parseProjectOverride(options.project),
382
317
  });
383
318
  const payload = { name, identifier };
384
319
  if (options.description !== undefined) {
@@ -408,7 +343,7 @@ export function registerProjectCommands(project, runtime) {
408
343
  .option("--json", "emit a flat JSON record")
409
344
  .option("--fields <list>", "comma-separated fields to show")
410
345
  .option("--profile <name>", "use this profile for this command only")
411
- .option("--project <id>", "override the profile default project")
346
+ .option("--project <name-or-id>", "override the profile default project")
412
347
  .action(async (reference, options) => {
413
348
  const isPublic = optionalBooleanFlag(options.public !== undefined, options.public, "public");
414
349
  const touchesSomething = options.name !== undefined
@@ -420,7 +355,7 @@ export function registerProjectCommands(project, runtime) {
420
355
  const { getPage } = await connect(runtime, options);
421
356
  const profile = await runtime.resolve({
422
357
  profile: options.profile,
423
- project: parseOptionalId(options.project),
358
+ project: parseProjectOverride(options.project),
424
359
  });
425
360
  const hit = await resolveProjectRef(getPage, reference);
426
361
  const payload = {};
@@ -447,13 +382,13 @@ export function registerProjectCommands(project, runtime) {
447
382
  .option("--json", "emit a flat JSON record")
448
383
  .option("--fields <list>", "comma-separated fields to show")
449
384
  .option("--profile <name>", "use this profile for this command only")
450
- .option("--project <id>", "override the profile default project")
385
+ .option("--project <name-or-id>", "override the profile default project")
451
386
  .action(async (reference, name, options) => {
452
387
  const identifier = requireIdentifier(options, "copy");
453
388
  const { getPage } = await connect(runtime, options);
454
389
  const profile = await runtime.resolve({
455
390
  profile: options.profile,
456
- project: parseOptionalId(options.project),
391
+ project: parseProjectOverride(options.project),
457
392
  });
458
393
  const hit = await resolveProjectRef(getPage, reference);
459
394
  const source = (await getPage(`${PROJECTS_COLLECTION}/${String(hit.id)}`));
@@ -478,12 +413,12 @@ export function registerProjectCommands(project, runtime) {
478
413
  .description("Mark one project as favourited for the current user")
479
414
  .argument("<reference>")
480
415
  .option("--profile <name>", "use this profile for this command only")
481
- .option("--project <id>", "override the profile default project")
416
+ .option("--project <name-or-id>", "override the profile default project")
482
417
  .action(async (reference, options) => {
483
418
  const { getPage } = await connect(runtime, options);
484
419
  const profile = await runtime.resolve({
485
420
  profile: options.profile,
486
- project: parseOptionalId(options.project),
421
+ project: parseProjectOverride(options.project),
487
422
  });
488
423
  const hit = await resolveProjectRef(getPage, reference);
489
424
  await apiPost(profile.instanceUrl, profile.apiKey, `${PROJECTS_COLLECTION}/${String(hit.id)}/favorite`, {});
@@ -494,12 +429,12 @@ export function registerProjectCommands(project, runtime) {
494
429
  .description("Remove one project from the current user's favourites")
495
430
  .argument("<reference>")
496
431
  .option("--profile <name>", "use this profile for this command only")
497
- .option("--project <id>", "override the profile default project")
432
+ .option("--project <name-or-id>", "override the profile default project")
498
433
  .action(async (reference, options) => {
499
434
  const { getPage } = await connect(runtime, options);
500
435
  const profile = await runtime.resolve({
501
436
  profile: options.profile,
502
- project: parseOptionalId(options.project),
437
+ project: parseProjectOverride(options.project),
503
438
  });
504
439
  const hit = await resolveProjectRef(getPage, reference);
505
440
  await apiDelete(profile.instanceUrl, profile.apiKey, `${PROJECTS_COLLECTION}/${String(hit.id)}/favorite`);
@@ -519,7 +454,7 @@ export function registerProjectCommands(project, runtime) {
519
454
  .option("--json", "emit a flat JSON array")
520
455
  .option("--fields <list>", "comma-separated columns to show")
521
456
  .option("--profile <name>", "use this profile for this command only")
522
- .option("--project <id>", "override the profile default project")
457
+ .option("--project <name-or-id>", "override the profile default project")
523
458
  .action(async (reference, options) => {
524
459
  const { getPage } = await connect(runtime, options);
525
460
  const hit = await resolveProjectRef(getPage, reference);
@@ -547,13 +482,13 @@ export function registerProjectCommands(project, runtime) {
547
482
  .option("--json", "emit a flat JSON record")
548
483
  .option("--fields <list>", "comma-separated fields to show")
549
484
  .option("--profile <name>", "use this profile for this command only")
550
- .option("--project <id>", "override the profile default project")
485
+ .option("--project <name-or-id>", "override the profile default project")
551
486
  .action(async (reference, principal, roles, options) => {
552
487
  runtime.setJsonMode(options.json === true);
553
488
  const { getPage } = await connect(runtime, options);
554
489
  const profile = await runtime.resolve({
555
490
  profile: options.profile,
556
- project: parseOptionalId(options.project),
491
+ project: parseProjectOverride(options.project),
557
492
  });
558
493
  const hit = await resolveProjectRef(getPage, reference);
559
494
  const principalId = await resolvePrincipalId(profile, principal);
@@ -576,12 +511,12 @@ export function registerProjectCommands(project, runtime) {
576
511
  .argument("<reference>", "project, by id, identifier, or name")
577
512
  .argument("<principal>", "user or group, by name, id, or me")
578
513
  .option("--profile <name>", "use this profile for this command only")
579
- .option("--project <id>", "override the profile default project")
514
+ .option("--project <name-or-id>", "override the profile default project")
580
515
  .action(async (reference, principal, options) => {
581
516
  const { getPage } = await connect(runtime, options);
582
517
  const profile = await runtime.resolve({
583
518
  profile: options.profile,
584
- project: parseOptionalId(options.project),
519
+ project: parseProjectOverride(options.project),
585
520
  });
586
521
  const hit = await resolveProjectRef(getPage, reference);
587
522
  const principalId = await resolvePrincipalId(profile, principal);
@@ -624,7 +559,7 @@ export function registerProjectCommands(project, runtime) {
624
559
  .argument("<reference>", "project, by id, identifier, or name")
625
560
  .option("--yes", "confirm the deletion")
626
561
  .option("--profile <name>", "use this profile for this command only")
627
- .option("--project <id>", "override the profile default project")
562
+ .option("--project <name-or-id>", "override the profile default project")
628
563
  .action(async (reference, options) => {
629
564
  // The guard fires before any resolution or traffic: without --yes
630
565
  // nothing is read, nothing is sent, with or without a terminal.
@@ -635,7 +570,7 @@ export function registerProjectCommands(project, runtime) {
635
570
  const { getPage } = await connect(runtime, options);
636
571
  const profile = await runtime.resolve({
637
572
  profile: options.profile,
638
- project: parseOptionalId(options.project),
573
+ project: parseProjectOverride(options.project),
639
574
  });
640
575
  const hit = await resolveProjectRef(getPage, reference);
641
576
  await apiDelete(profile.instanceUrl, profile.apiKey, `${PROJECTS_COLLECTION}/${String(hit.id)}`);