@elmundi/ship-cli 0.15.4 → 0.16.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/bin/shipctl.mjs +12 -0
- package/lib/agent_api.mjs +192 -0
- package/lib/commands/inbox.mjs +171 -0
- package/lib/commands/project.mjs +152 -0
- package/package.json +1 -1
package/bin/shipctl.mjs
CHANGED
|
@@ -81,6 +81,18 @@ try {
|
|
|
81
81
|
process.exit(0);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
if (cmd === "inbox") {
|
|
85
|
+
const { inboxCommand } = await import("../lib/commands/inbox.mjs");
|
|
86
|
+
await inboxCommand(ctx, rest);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (cmd === "project") {
|
|
91
|
+
const { projectCommand } = await import("../lib/commands/project.mjs");
|
|
92
|
+
await projectCommand(ctx, rest);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
if (cmd === "run") {
|
|
85
97
|
const { runCommand } = await import("../lib/commands/run.mjs");
|
|
86
98
|
await runCommand(ctx, rest);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared client helpers for ``shipctl`` commands that hit the
|
|
3
|
+
* agent-runs API (inbox / project / etc).
|
|
4
|
+
*
|
|
5
|
+
* Reuses the same auth model the existing ``shipctl knowledge`` flow
|
|
6
|
+
* established: bearer ``SHIP_API_TOKEN`` PAT, base URL resolved via
|
|
7
|
+
* the same precedence chain (flag → ``SHIP_WORKSPACE_API_BASE`` →
|
|
8
|
+
* ``SHIP_API_BASE`` → prod default), workspace id resolved via flag /
|
|
9
|
+
* ``SHIP_WORKSPACE_ID`` / sole-workspace lookup.
|
|
10
|
+
*
|
|
11
|
+
* Exit codes are uniform across commands so wrapper scripts can
|
|
12
|
+
* branch on them: 0 ok, 1 arg/config error, 2 auth error (401),
|
|
13
|
+
* 3 network/5xx.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const PROD_BASE = "https://api.ship.elmundi.com";
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string|null|undefined} explicit
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function resolveBaseUrl(explicit) {
|
|
24
|
+
if (explicit) return explicit.replace(/\/+$/, "");
|
|
25
|
+
const envWorkspace = process.env.SHIP_WORKSPACE_API_BASE;
|
|
26
|
+
if (envWorkspace) return envWorkspace.replace(/\/+$/, "");
|
|
27
|
+
const envGeneric = process.env.SHIP_API_BASE;
|
|
28
|
+
if (envGeneric) return envGeneric.replace(/\/+$/, "");
|
|
29
|
+
return PROD_BASE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export function requireToken() {
|
|
34
|
+
const token = process.env.SHIP_API_TOKEN || "";
|
|
35
|
+
if (!token) {
|
|
36
|
+
console.error(
|
|
37
|
+
"SHIP_API_TOKEN is required. Mint one at /settings in the Ship console.",
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return token;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} baseUrl
|
|
47
|
+
* @param {string} token
|
|
48
|
+
* @returns {Promise<string>}
|
|
49
|
+
*/
|
|
50
|
+
export async function resolveSoleWorkspace(baseUrl, token) {
|
|
51
|
+
const rows = await apiRequest(baseUrl, "/v1/workspaces", "GET", token, null);
|
|
52
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
53
|
+
console.error("No workspaces visible to this token.");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if (rows.length > 1) {
|
|
57
|
+
const ids = rows.map((r) => `${r.id} (${r.name ?? "?"})`).join("\n ");
|
|
58
|
+
console.error(
|
|
59
|
+
`Token has access to more than one workspace; pass --workspace <id>.\n ${ids}`,
|
|
60
|
+
);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
return String(rows[0].id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {{ workspace?: string|null, baseUrl?: string|null }} opts
|
|
69
|
+
* @param {{ baseUrl?: string|null, baseUrlSource?: string|null }} ctx
|
|
70
|
+
*/
|
|
71
|
+
export async function resolveContext(opts, ctx) {
|
|
72
|
+
const baseUrl = resolveBaseUrl(
|
|
73
|
+
opts.baseUrl || (ctx?.baseUrlSource === "flag" ? ctx?.baseUrl : null),
|
|
74
|
+
);
|
|
75
|
+
const token = requireToken();
|
|
76
|
+
let workspaceId =
|
|
77
|
+
opts.workspace || (process.env.SHIP_WORKSPACE_ID || "").trim() || "";
|
|
78
|
+
if (!workspaceId) {
|
|
79
|
+
workspaceId = await resolveSoleWorkspace(baseUrl, token);
|
|
80
|
+
}
|
|
81
|
+
return { baseUrl, token, workspaceId };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} baseUrl
|
|
87
|
+
* @param {string} path
|
|
88
|
+
* @param {"GET"|"POST"|"DELETE"} method
|
|
89
|
+
* @param {string} token
|
|
90
|
+
* @param {Record<string, unknown>|null} body
|
|
91
|
+
*/
|
|
92
|
+
export async function apiRequest(baseUrl, path, method, token, body) {
|
|
93
|
+
const url = `${baseUrl}${path}`;
|
|
94
|
+
let res;
|
|
95
|
+
try {
|
|
96
|
+
res = await fetch(url, {
|
|
97
|
+
method,
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
Accept: "application/json",
|
|
101
|
+
Authorization: `Bearer ${token}`,
|
|
102
|
+
},
|
|
103
|
+
body: body === null ? undefined : JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(
|
|
107
|
+
`Network error calling ${url}: ${err instanceof Error ? err.message : err}`,
|
|
108
|
+
);
|
|
109
|
+
process.exit(3);
|
|
110
|
+
}
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
let data = null;
|
|
113
|
+
try {
|
|
114
|
+
data = text ? JSON.parse(text) : null;
|
|
115
|
+
} catch {
|
|
116
|
+
data = text;
|
|
117
|
+
}
|
|
118
|
+
if (res.ok) return data;
|
|
119
|
+
if (res.status === 401) {
|
|
120
|
+
console.error(
|
|
121
|
+
`HTTP 401 on ${method} ${url} — SHIP_API_TOKEN is missing, expired, or lacks workspace access.`,
|
|
122
|
+
);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
const msg = typeof data === "string" ? data : JSON.stringify(data);
|
|
126
|
+
console.error(`HTTP ${res.status} ${res.statusText} on ${method} ${url}\n${msg}`);
|
|
127
|
+
process.exit(res.status >= 500 ? 3 : 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read a value from a flag (`--name`/`--name=value`). Returns true if
|
|
133
|
+
* consumed; mutates ``copy`` in place.
|
|
134
|
+
*
|
|
135
|
+
* @param {string[]} copy
|
|
136
|
+
* @param {string} flag
|
|
137
|
+
* @param {Record<string, string|null>} out
|
|
138
|
+
* @param {string} key
|
|
139
|
+
*/
|
|
140
|
+
export function consumeStringFlag(copy, flag, out, key) {
|
|
141
|
+
if (copy[0] === flag && copy[1] !== undefined) {
|
|
142
|
+
copy.shift();
|
|
143
|
+
out[key] = String(copy.shift());
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
const p = `${flag}=`;
|
|
147
|
+
if (copy[0] && copy[0].startsWith(p)) {
|
|
148
|
+
out[key] = copy[0].slice(p.length);
|
|
149
|
+
copy.shift();
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Read a body / file argument. ``--body <inline>`` takes the next arg
|
|
158
|
+
* as the literal body; ``--body-file <path>`` reads the file content.
|
|
159
|
+
* Mutually exclusive — the second wins if both are given but we warn.
|
|
160
|
+
*
|
|
161
|
+
* @param {string[]} copy
|
|
162
|
+
* @param {Record<string, string|null>} out
|
|
163
|
+
*/
|
|
164
|
+
export function consumeBodyFlags(copy, out) {
|
|
165
|
+
if (consumeStringFlag(copy, "--body", out, "body")) return true;
|
|
166
|
+
if (consumeStringFlag(copy, "--body-file", out, "bodyFile")) return true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve ``--body`` vs ``--body-file`` into a single string. Reads
|
|
173
|
+
* stdin when ``--body-file=-`` so callers can pipe markdown.
|
|
174
|
+
*
|
|
175
|
+
* @param {{ body?: string|null, bodyFile?: string|null }} out
|
|
176
|
+
*/
|
|
177
|
+
export async function readBodyFromOpts(out) {
|
|
178
|
+
if (out.body && out.bodyFile) {
|
|
179
|
+
console.error("Pass --body OR --body-file, not both.");
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
if (out.bodyFile) {
|
|
183
|
+
const fs = await import("node:fs/promises");
|
|
184
|
+
if (out.bodyFile === "-") {
|
|
185
|
+
const chunks = [];
|
|
186
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
187
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
188
|
+
}
|
|
189
|
+
return await fs.readFile(out.bodyFile, "utf8");
|
|
190
|
+
}
|
|
191
|
+
return out.body || "";
|
|
192
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shipctl inbox` — write into the operator's mailbox from a routine.
|
|
3
|
+
*
|
|
4
|
+
* Routines like daily-retro / learning-capture / process-reviewer file
|
|
5
|
+
* a single ``type=report`` letter per run; the operator opens it in the
|
|
6
|
+
* mailbox preview and hits Acknowledge. SDLC pipelines occasionally
|
|
7
|
+
* file ``approval`` / ``improvement`` items.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
*
|
|
11
|
+
* shipctl inbox create
|
|
12
|
+
* --type <report|improvement|approval|exception>
|
|
13
|
+
* --title "Daily retro 2026-05-07"
|
|
14
|
+
* --body "<markdown>" | --body-file path/to/digest.md | --body-file -
|
|
15
|
+
* [--summary "<short list-row blurb>"]
|
|
16
|
+
* [--workspace <id>]
|
|
17
|
+
* [--ticket-ref <id>]
|
|
18
|
+
* [--json]
|
|
19
|
+
*
|
|
20
|
+
* Auth: bearer ``SHIP_API_TOKEN`` (mint at /settings).
|
|
21
|
+
*
|
|
22
|
+
* Exit codes:
|
|
23
|
+
* 0 inbox item created
|
|
24
|
+
* 1 arg / config / 4xx error
|
|
25
|
+
* 2 auth error (401)
|
|
26
|
+
* 3 network / 5xx error
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
apiRequest,
|
|
31
|
+
consumeBodyFlags,
|
|
32
|
+
consumeStringFlag,
|
|
33
|
+
readBodyFromOpts,
|
|
34
|
+
resolveContext,
|
|
35
|
+
} from "../agent_api.mjs";
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
const VALID_TYPES = new Set([
|
|
39
|
+
"report",
|
|
40
|
+
"improvement",
|
|
41
|
+
"approval",
|
|
42
|
+
"exception",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export async function inboxCommand(ctx, rest) {
|
|
47
|
+
const [sub, ...args] = rest;
|
|
48
|
+
if (!sub || sub === "help" || sub === "-h" || sub === "--help") {
|
|
49
|
+
printInboxHelp();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (sub === "create") {
|
|
53
|
+
await inboxCreateCommand(ctx, args);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.error(
|
|
57
|
+
`Unknown 'shipctl inbox' subcommand: ${sub}\nRun: shipctl inbox --help`,
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
function printInboxHelp() {
|
|
64
|
+
console.log(`shipctl inbox — file letters in the operator's mailbox
|
|
65
|
+
|
|
66
|
+
SUBCOMMANDS
|
|
67
|
+
shipctl inbox create --type <kind> --title <s> --body <md>
|
|
68
|
+
[--summary <s>] [--ticket-ref <id>]
|
|
69
|
+
[--body-file <path|->] [--workspace <id>] [--json]
|
|
70
|
+
|
|
71
|
+
TYPES
|
|
72
|
+
report Read-only digest (daily retro, learning capture,
|
|
73
|
+
process review). Operator hits Acknowledge.
|
|
74
|
+
improvement Actionable suggestion. Operator accepts / dismisses.
|
|
75
|
+
approval Gated decision. Operator approves / rejects / dismisses.
|
|
76
|
+
exception Policy edge case. Operator acknowledges / dismisses.
|
|
77
|
+
|
|
78
|
+
ENV
|
|
79
|
+
SHIP_API_TOKEN Required. Bearer PAT minted at /settings.
|
|
80
|
+
SHIP_WORKSPACE_ID Optional. Skips the /v1/workspaces lookup.
|
|
81
|
+
SHIP_WORKSPACE_API_BASE Optional override for the control plane.
|
|
82
|
+
|
|
83
|
+
EXIT
|
|
84
|
+
0 letter filed
|
|
85
|
+
1 arg / config / 4xx
|
|
86
|
+
2 auth (401)
|
|
87
|
+
3 network / 5xx
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async function inboxCreateCommand(ctx, args) {
|
|
93
|
+
const opts = parseCreateArgs(args);
|
|
94
|
+
const body = (await readBodyFromOpts(opts)).trim();
|
|
95
|
+
if (!body) {
|
|
96
|
+
console.error("Pass --body <markdown> or --body-file <path|->");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const { baseUrl, token, workspaceId } = await resolveContext(opts, ctx);
|
|
100
|
+
|
|
101
|
+
const result = await apiRequest(
|
|
102
|
+
baseUrl,
|
|
103
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/inbox/items`,
|
|
104
|
+
"POST",
|
|
105
|
+
token,
|
|
106
|
+
{
|
|
107
|
+
type: opts.type,
|
|
108
|
+
title: opts.title,
|
|
109
|
+
body,
|
|
110
|
+
summary: opts.summary || null,
|
|
111
|
+
ticket_ref: opts.ticketRef || null,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (ctx?.json || opts.json) {
|
|
116
|
+
console.log(JSON.stringify(result, null, 2));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log(`Letter filed (${opts.type}): ${opts.title}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
function parseCreateArgs(args) {
|
|
124
|
+
const out = {
|
|
125
|
+
type: null,
|
|
126
|
+
title: null,
|
|
127
|
+
body: null,
|
|
128
|
+
bodyFile: null,
|
|
129
|
+
summary: null,
|
|
130
|
+
ticketRef: null,
|
|
131
|
+
workspace: null,
|
|
132
|
+
baseUrl: null,
|
|
133
|
+
json: false,
|
|
134
|
+
};
|
|
135
|
+
const copy = [...args];
|
|
136
|
+
while (copy.length) {
|
|
137
|
+
if (
|
|
138
|
+
consumeStringFlag(copy, "--type", out, "type") ||
|
|
139
|
+
consumeStringFlag(copy, "--title", out, "title") ||
|
|
140
|
+
consumeStringFlag(copy, "--summary", out, "summary") ||
|
|
141
|
+
consumeStringFlag(copy, "--ticket-ref", out, "ticketRef") ||
|
|
142
|
+
consumeStringFlag(copy, "--workspace", out, "workspace") ||
|
|
143
|
+
consumeStringFlag(copy, "--base-url", out, "baseUrl") ||
|
|
144
|
+
consumeBodyFlags(copy, out)
|
|
145
|
+
) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (copy[0] === "--json") {
|
|
149
|
+
out.json = true;
|
|
150
|
+
copy.shift();
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (copy[0] === "--help" || copy[0] === "-h") {
|
|
154
|
+
printInboxHelp();
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
console.error(`Unknown flag: ${copy[0]}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
if (!out.type || !VALID_TYPES.has(out.type)) {
|
|
161
|
+
console.error(
|
|
162
|
+
`--type must be one of ${[...VALID_TYPES].join(" | ")}`,
|
|
163
|
+
);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
if (!out.title || !out.title.trim()) {
|
|
167
|
+
console.error("--title is required");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shipctl project` — manage tracker projects from a routine.
|
|
3
|
+
*
|
|
4
|
+
* Today the only subcommand is ``find-or-create``: the reviewer
|
|
5
|
+
* routines (tech-reviewer, qa-reviewer, security-officer) call this
|
|
6
|
+
* once per run to resolve their dedicated holding-pen project ("Tech
|
|
7
|
+
* Debt", "QA Debt", "Security") on the workspace's bound tracker.
|
|
8
|
+
* First run creates; every subsequent run short-circuits on the
|
|
9
|
+
* case-insensitive name match — no list-then-create race.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
*
|
|
13
|
+
* shipctl project find-or-create
|
|
14
|
+
* --name "Tech Debt"
|
|
15
|
+
* --body "<markdown body for the new project — only used on create>"
|
|
16
|
+
* [--description "<one-liner; ≤240 chars; only used on create>"]
|
|
17
|
+
* [--workspace <id>]
|
|
18
|
+
* [--json]
|
|
19
|
+
*
|
|
20
|
+
* Output (default): ``project_id\tname\tcreated`` on a single line so
|
|
21
|
+
* shell scripts can parse it. ``--json`` returns the full server
|
|
22
|
+
* response.
|
|
23
|
+
*
|
|
24
|
+
* Auth: bearer ``SHIP_API_TOKEN``.
|
|
25
|
+
*
|
|
26
|
+
* Exit codes:
|
|
27
|
+
* 0 project resolved (found or created)
|
|
28
|
+
* 1 arg / config / 4xx error
|
|
29
|
+
* 2 auth error (401)
|
|
30
|
+
* 3 network / 5xx error
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
apiRequest,
|
|
35
|
+
consumeBodyFlags,
|
|
36
|
+
consumeStringFlag,
|
|
37
|
+
readBodyFromOpts,
|
|
38
|
+
resolveContext,
|
|
39
|
+
} from "../agent_api.mjs";
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
export async function projectCommand(ctx, rest) {
|
|
43
|
+
const [sub, ...args] = rest;
|
|
44
|
+
if (!sub || sub === "help" || sub === "-h" || sub === "--help") {
|
|
45
|
+
printProjectHelp();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (sub === "find-or-create") {
|
|
49
|
+
await projectFindOrCreateCommand(ctx, args);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.error(
|
|
53
|
+
`Unknown 'shipctl project' subcommand: ${sub}\nRun: shipctl project --help`,
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
function printProjectHelp() {
|
|
60
|
+
console.log(`shipctl project — manage tracker projects
|
|
61
|
+
|
|
62
|
+
SUBCOMMANDS
|
|
63
|
+
shipctl project find-or-create --name <s> --body <md>
|
|
64
|
+
[--description <s>] [--body-file <path|->]
|
|
65
|
+
[--workspace <id>] [--json]
|
|
66
|
+
|
|
67
|
+
ENV
|
|
68
|
+
SHIP_API_TOKEN Required. Bearer PAT minted at /settings.
|
|
69
|
+
SHIP_WORKSPACE_ID Optional. Skips the /v1/workspaces lookup.
|
|
70
|
+
SHIP_WORKSPACE_API_BASE Optional override for the control plane.
|
|
71
|
+
|
|
72
|
+
EXIT
|
|
73
|
+
0 project resolved
|
|
74
|
+
1 arg / config / 4xx
|
|
75
|
+
2 auth (401)
|
|
76
|
+
3 network / 5xx
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async function projectFindOrCreateCommand(ctx, args) {
|
|
82
|
+
const opts = parseFindOrCreateArgs(args);
|
|
83
|
+
const body = (await readBodyFromOpts(opts)).trim();
|
|
84
|
+
if (!body) {
|
|
85
|
+
console.error("Pass --body <markdown> or --body-file <path|->");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const { baseUrl, token, workspaceId } = await resolveContext(opts, ctx);
|
|
89
|
+
|
|
90
|
+
const result = await apiRequest(
|
|
91
|
+
baseUrl,
|
|
92
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/projects/find-or-create`,
|
|
93
|
+
"POST",
|
|
94
|
+
token,
|
|
95
|
+
{
|
|
96
|
+
name: opts.name,
|
|
97
|
+
body,
|
|
98
|
+
description: opts.description || null,
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (ctx?.json || opts.json) {
|
|
103
|
+
console.log(JSON.stringify(result, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const project = result?.project ?? {};
|
|
107
|
+
const created = result?.created ? "created" : "existing";
|
|
108
|
+
console.log(
|
|
109
|
+
`${project.id ?? ""}\t${project.name ?? opts.name}\t${created}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
function parseFindOrCreateArgs(args) {
|
|
115
|
+
const out = {
|
|
116
|
+
name: null,
|
|
117
|
+
body: null,
|
|
118
|
+
bodyFile: null,
|
|
119
|
+
description: null,
|
|
120
|
+
workspace: null,
|
|
121
|
+
baseUrl: null,
|
|
122
|
+
json: false,
|
|
123
|
+
};
|
|
124
|
+
const copy = [...args];
|
|
125
|
+
while (copy.length) {
|
|
126
|
+
if (
|
|
127
|
+
consumeStringFlag(copy, "--name", out, "name") ||
|
|
128
|
+
consumeStringFlag(copy, "--description", out, "description") ||
|
|
129
|
+
consumeStringFlag(copy, "--workspace", out, "workspace") ||
|
|
130
|
+
consumeStringFlag(copy, "--base-url", out, "baseUrl") ||
|
|
131
|
+
consumeBodyFlags(copy, out)
|
|
132
|
+
) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (copy[0] === "--json") {
|
|
136
|
+
out.json = true;
|
|
137
|
+
copy.shift();
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (copy[0] === "--help" || copy[0] === "-h") {
|
|
141
|
+
printProjectHelp();
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
console.error(`Unknown flag: ${copy[0]}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
if (!out.name || !out.name.trim()) {
|
|
148
|
+
console.error("--name is required");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|