@elmundi/ship-cli 0.12.1 → 0.12.2
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 +6 -0
- package/lib/commands/help.mjs +27 -20
- package/lib/commands/knowledge.mjs +18 -7
- package/lib/commands/process.mjs +388 -0
- package/lib/commands/run.mjs +76 -52
- package/lib/commands/trigger.mjs +73 -40
- package/lib/config/schema.mjs +193 -7
- package/lib/process/specialist-prompt-contract.mjs +171 -0
- package/lib/runtime/routines.mjs +264 -0
- package/package.json +4 -4
package/bin/shipctl.mjs
CHANGED
|
@@ -140,6 +140,12 @@ try {
|
|
|
140
140
|
process.exit(0);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
if (cmd === "process") {
|
|
144
|
+
const { processCommand } = await import("../lib/commands/process.mjs");
|
|
145
|
+
await processCommand(ctx, rest);
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
143
149
|
if (cmd === "migrate") {
|
|
144
150
|
const { migrateCommand } = await import("../lib/commands/migrate.mjs");
|
|
145
151
|
await migrateCommand(ctx, rest);
|
package/lib/commands/help.mjs
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
export function printHelp() {
|
|
2
|
-
console.log(`shipctl — adopt Ship in a repo, sync the catalog, run
|
|
2
|
+
console.log(`shipctl — adopt Ship in a repo, sync the catalog, run routines, report outcomes.
|
|
3
3
|
|
|
4
4
|
Bootstrap a new or existing repo (init / new / doctor), pull the
|
|
5
|
-
methodology catalog into .ship/cache (sync), execute one-shot
|
|
6
|
-
emit prompts for the workspace runner (
|
|
5
|
+
methodology catalog into .ship/cache (sync), execute one-shot routines or
|
|
6
|
+
emit prompts for the workspace runner (trigger / run / kickoff /
|
|
7
7
|
callback). Talks to the methodology + orchestration APIs over HTTPS.
|
|
8
8
|
|
|
9
9
|
VOCABULARY
|
|
10
|
-
|
|
11
|
-
pattern:
|
|
12
|
-
|
|
13
|
-
attention surface
|
|
10
|
+
process.routines: (.ship/config.yml) → repo-local scheduled/manual work
|
|
11
|
+
pattern: (artifact kind) → cached role/playbook prompt body
|
|
12
|
+
routine claim: (Ship API) → idempotent schedule-window claim
|
|
13
|
+
attention surface → operator console: Inbox
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Legacy 'lanes:' configs and '--lane' flags are still accepted as aliases
|
|
16
|
+
so already-seeded repositories keep working while new repos use
|
|
17
|
+
'process.routines'.
|
|
18
18
|
|
|
19
19
|
GLOBAL FLAGS
|
|
20
20
|
--base-url URL Methodology API (default: SHIP_API_BASE or
|
|
@@ -66,22 +66,18 @@ COMMANDS
|
|
|
66
66
|
[--force-unpin] [--dry-run] [--lock] [--json] [--cwd <dir>]
|
|
67
67
|
— fetch artifacts into .ship/cache. With --lock,
|
|
68
68
|
also writes .ship/shipctl.lock.json covering
|
|
69
|
-
|
|
69
|
+
every pattern the declared routines depend on.
|
|
70
70
|
|
|
71
71
|
Run
|
|
72
72
|
shipctl trigger --event schedule --repo <id|owner/name> [--workspace <id>] [--json]
|
|
73
|
-
—
|
|
74
|
-
|
|
75
|
-
shipctl run --
|
|
73
|
+
— compute due routines locally, then claim
|
|
74
|
+
the schedule window in Ship.
|
|
75
|
+
shipctl run --routine <id> [--pattern <id>] [--fanout matrix|sequential|concurrent]
|
|
76
76
|
[--trigger event|schedule|manual|once]
|
|
77
77
|
[--dry-run] [--offline] [--json] [--cwd <dir>]
|
|
78
78
|
[--ship-run-id <uuid>] [--ship-callback-url <url>] [--ship-run-token <jwt>]
|
|
79
|
-
— one-shot dispatch entry point.
|
|
80
|
-
|
|
81
|
-
schedule' lanes are queued for the workspace
|
|
82
|
-
runner via .github/workflows/run-agent.yml.
|
|
83
|
-
Reports its terminal status via the callback URL
|
|
84
|
-
Ship injected into the workflow.
|
|
79
|
+
— one-shot routine dispatch entry point.
|
|
80
|
+
Use --lane as a legacy alias.
|
|
85
81
|
shipctl lanes install [--only <csv>] [--ref <git-ref>] [--owner <gh>] [--repo <name>]
|
|
86
82
|
[--shipctl-version <v>] [--dry-run] [--force] [--json] [--cwd <dir>]
|
|
87
83
|
shipctl lanes list [--json] [--cwd <dir>]
|
|
@@ -99,6 +95,17 @@ COMMANDS
|
|
|
99
95
|
RunSummary outcome) back to Ship so it can
|
|
100
96
|
render the outcome row and route any
|
|
101
97
|
escalations into the Inbox.
|
|
98
|
+
shipctl process prompt --state <id> [--ticket-json <json>] [--policies-file <path>]
|
|
99
|
+
[--cwd <dir>] [--json]
|
|
100
|
+
— assemble a Process/FSM specialist prompt
|
|
101
|
+
bundle with ticket context, allowed
|
|
102
|
+
transitions, policies, and mandatory
|
|
103
|
+
knowledge-first guardrails.
|
|
104
|
+
shipctl process tickets --workspace <id> [--query <text>] [--tracker <kind>] [--json]
|
|
105
|
+
— read-only tracker picker for selecting
|
|
106
|
+
ticket context before building a process
|
|
107
|
+
prompt. Does not create, comment, or
|
|
108
|
+
transition tickets.
|
|
102
109
|
|
|
103
110
|
Knowledge
|
|
104
111
|
shipctl knowledge init [--workspace <id>] [--repo <id|owner/name>] [--only <csv>] [--json]
|
|
@@ -44,10 +44,12 @@
|
|
|
44
44
|
|
|
45
45
|
const VERSION = "v1";
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
* ``
|
|
50
|
-
|
|
47
|
+
/** Static starters with source markdown under ``artifacts/knowledge-starters``.
|
|
48
|
+
* Procedural catalog recipes are exposed by the backend under the
|
|
49
|
+
* ``ship-recipes/<pattern-id>`` prefix because that list is generated from
|
|
50
|
+
* on-disk pattern artifacts at runtime. */
|
|
51
|
+
export const STATIC_KNOWLEDGE_SLUGS = ["code-style", "ui-runbook"];
|
|
52
|
+
export const RECIPE_KNOWLEDGE_PREFIX = "ship-recipes/";
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
55
|
* @param {{baseUrl?: string, json?: boolean}} ctx
|
|
@@ -100,7 +102,9 @@ INIT FLAGS
|
|
|
100
102
|
Defaults to the most-recently activated repo in
|
|
101
103
|
the resolved workspace.
|
|
102
104
|
--only <csv> Comma-separated starter slugs. Defaults to the
|
|
103
|
-
full catalog
|
|
105
|
+
full backend catalog, including static starters
|
|
106
|
+
(${STATIC_KNOWLEDGE_SLUGS.join(", ")}) and generated
|
|
107
|
+
recipe starters under ${RECIPE_KNOWLEDGE_PREFIX}<pattern-id>.
|
|
104
108
|
--base-url URL Workspace control-plane API. See env fallbacks.
|
|
105
109
|
--json Emit a machine-readable JSON summary.
|
|
106
110
|
|
|
@@ -134,10 +138,10 @@ async function knowledgeInitCommand(ctx, args) {
|
|
|
134
138
|
|
|
135
139
|
const selection = opts.only;
|
|
136
140
|
if (selection !== null) {
|
|
137
|
-
const unknown = selection.filter((s) => !
|
|
141
|
+
const unknown = selection.filter((s) => !isKnownKnowledgeStarterSlug(s));
|
|
138
142
|
if (unknown.length) {
|
|
139
143
|
console.error(
|
|
140
|
-
`Unknown knowledge slug(s): ${unknown.join(", ")}\nKnown: ${
|
|
144
|
+
`Unknown knowledge slug(s): ${unknown.join(", ")}\nKnown static slugs: ${STATIC_KNOWLEDGE_SLUGS.join(", ")}; recipe slugs must start with ${RECIPE_KNOWLEDGE_PREFIX}`,
|
|
141
145
|
);
|
|
142
146
|
process.exit(1);
|
|
143
147
|
}
|
|
@@ -173,6 +177,13 @@ async function knowledgeInitCommand(ctx, args) {
|
|
|
173
177
|
);
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
export function isKnownKnowledgeStarterSlug(slug) {
|
|
181
|
+
return (
|
|
182
|
+
STATIC_KNOWLEDGE_SLUGS.includes(slug) ||
|
|
183
|
+
slug.startsWith(RECIPE_KNOWLEDGE_PREFIX)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
176
187
|
async function knowledgeFetchCommand(ctx, args) {
|
|
177
188
|
const opts = parseFetchArgs(args);
|
|
178
189
|
const baseUrl = resolveBaseUrl(opts.baseUrl || explicitGlobalBaseUrl(ctx));
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { readConfig } from "../config/io.mjs";
|
|
5
|
+
import { CONFIG_SCHEMA_VERSION, validateConfig } from "../config/schema.mjs";
|
|
6
|
+
import { apiGet } from "../http.mjs";
|
|
7
|
+
import {
|
|
8
|
+
buildSpecialistPromptBundle,
|
|
9
|
+
renderSpecialistPromptBundleMarkdown,
|
|
10
|
+
} from "../process/specialist-prompt-contract.mjs";
|
|
11
|
+
|
|
12
|
+
export async function processCommand(ctx, rest) {
|
|
13
|
+
const [sub, ...args] = rest;
|
|
14
|
+
if (!sub || sub === "help" || sub === "-h" || sub === "--help") {
|
|
15
|
+
printProcessHelp();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (sub === "prompt" || sub === "bundle") {
|
|
19
|
+
await processPromptCommand(ctx, args);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (sub === "tickets") {
|
|
23
|
+
await processTicketsCommand(ctx, args);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.error(
|
|
27
|
+
`Unknown 'shipctl process' subcommand: ${sub}\nRun: shipctl process --help`,
|
|
28
|
+
);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function printProcessHelp() {
|
|
33
|
+
console.log(`shipctl process — assemble Process/FSM specialist context
|
|
34
|
+
|
|
35
|
+
USAGE
|
|
36
|
+
shipctl process prompt --state <state-id> [--ticket-json <json>] [--ticket-file <path>]
|
|
37
|
+
[--ticket-id <id>] [--ticket-title <title>] [--ticket-url <url>]
|
|
38
|
+
[--ticket-description <text>] [--policies-file <path>]
|
|
39
|
+
[--cwd <dir>] [--json]
|
|
40
|
+
shipctl process prompt --specialist <specialist-id> [...]
|
|
41
|
+
shipctl process tickets --workspace <id> [--process <id>] [--tracker <kind>]
|
|
42
|
+
[--query <text>] [--state open|closed|all] [--limit <n>]
|
|
43
|
+
[--project-hint <hint>] [--assignee-me] [--assignee <user>]
|
|
44
|
+
[--json]
|
|
45
|
+
|
|
46
|
+
WHAT IT EMITS
|
|
47
|
+
A specialist prompt bundle assembled from .ship/config.yml:
|
|
48
|
+
- process and state identity
|
|
49
|
+
- specialist template fields from the selected state
|
|
50
|
+
- ticket context supplied by the caller / tracker picker
|
|
51
|
+
- allowed outgoing FSM transitions from process.transitions
|
|
52
|
+
- workspace policies supplied by --policies-file
|
|
53
|
+
- mandatory knowledge-first and Ship-boundary guardrails
|
|
54
|
+
|
|
55
|
+
The command does not mutate tracker tickets, execute transitions, or write to
|
|
56
|
+
the repository. It prepares context for an agent runtime; Ship remains
|
|
57
|
+
responsible for side effects, FSM validation, audit logging, and PR-only writes.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function processTicketsCommand(ctx, args) {
|
|
61
|
+
const opts = parseTicketsArgs(args);
|
|
62
|
+
const baseUrl = resolveWorkspaceBaseUrl(opts.baseUrl || ctx.baseUrl);
|
|
63
|
+
const token = process.env.SHIP_API_TOKEN || "";
|
|
64
|
+
if (!token) {
|
|
65
|
+
console.error("SHIP_API_TOKEN is required for shipctl process tickets.");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const processId = opts.processId || "development";
|
|
69
|
+
const params = new URLSearchParams();
|
|
70
|
+
for (const [key, value] of Object.entries({
|
|
71
|
+
tracker: opts.tracker,
|
|
72
|
+
project_hint: opts.projectHint,
|
|
73
|
+
state: opts.state,
|
|
74
|
+
query: opts.query,
|
|
75
|
+
assignee: opts.assignee,
|
|
76
|
+
limit: opts.limit,
|
|
77
|
+
})) {
|
|
78
|
+
if (value !== null && value !== undefined && value !== "") {
|
|
79
|
+
params.set(key, String(value));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (opts.assigneeMe) params.set("assignee_me", "true");
|
|
83
|
+
const suffix = params.toString() ? `?${params}` : "";
|
|
84
|
+
const data = await apiGet(
|
|
85
|
+
baseUrl,
|
|
86
|
+
`/v1/workspaces/${encodeURIComponent(opts.workspace)}/processes/${encodeURIComponent(processId)}/tickets${suffix}`,
|
|
87
|
+
);
|
|
88
|
+
if (ctx.json || opts.json) {
|
|
89
|
+
console.log(JSON.stringify(data, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const tickets = Array.isArray(data?.tickets) ? data.tickets : [];
|
|
93
|
+
console.log(`Tracker: ${data?.tracker || opts.tracker || "(auto)"}`);
|
|
94
|
+
if (!tickets.length) {
|
|
95
|
+
console.log("No tickets matched.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const ticket of tickets) {
|
|
99
|
+
const id = ticket.key || ticket.display_id || ticket.id || "(no id)";
|
|
100
|
+
const title = ticket.title || "(untitled)";
|
|
101
|
+
const status = ticket.status ? ` [${ticket.status}]` : "";
|
|
102
|
+
const url = ticket.url ? `\n ${ticket.url}` : "";
|
|
103
|
+
console.log(`- ${id}${status}: ${title}${url}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function processPromptCommand(ctx, args) {
|
|
108
|
+
const opts = parsePromptArgs(args);
|
|
109
|
+
const cwd = opts.cwd || process.cwd();
|
|
110
|
+
let config;
|
|
111
|
+
try {
|
|
112
|
+
config = readConfig(cwd).config;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
die(err instanceof Error ? err.message : String(err));
|
|
115
|
+
}
|
|
116
|
+
if (config.version !== CONFIG_SCHEMA_VERSION) {
|
|
117
|
+
die(
|
|
118
|
+
`.ship/config.yml is at v${config.version}; shipctl process prompt requires v${CONFIG_SCHEMA_VERSION}.\nRun 'shipctl migrate' to upgrade.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const validation = validateConfig(config);
|
|
122
|
+
if (!validation.ok) {
|
|
123
|
+
die(["config is invalid:", ...validation.errors.map((e) => ` - ${e}`)].join("\n"));
|
|
124
|
+
}
|
|
125
|
+
const processConfig = config.process;
|
|
126
|
+
if (!processConfig || typeof processConfig !== "object") {
|
|
127
|
+
die("process: missing from .ship/config.yml");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const state = selectState(processConfig, opts);
|
|
131
|
+
const allowedTransitions = Array.isArray(processConfig.transitions)
|
|
132
|
+
? processConfig.transitions.filter((transition) => transition.from === state.id)
|
|
133
|
+
: [];
|
|
134
|
+
const bundle = buildSpecialistPromptBundle({
|
|
135
|
+
process: processConfig,
|
|
136
|
+
state,
|
|
137
|
+
allowedTransitions,
|
|
138
|
+
ticket: resolveTicket(opts),
|
|
139
|
+
policies: readOptionalFile(opts.policiesFile, "policies"),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (ctx.json || opts.json) {
|
|
143
|
+
console.log(JSON.stringify(bundle, null, 2));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write(renderSpecialistPromptBundleMarkdown(bundle));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parsePromptArgs(args) {
|
|
150
|
+
const out = {
|
|
151
|
+
state: null,
|
|
152
|
+
specialist: null,
|
|
153
|
+
cwd: null,
|
|
154
|
+
json: false,
|
|
155
|
+
ticketJson: null,
|
|
156
|
+
ticketFile: null,
|
|
157
|
+
ticket: {},
|
|
158
|
+
policiesFile: null,
|
|
159
|
+
};
|
|
160
|
+
const copy = [...args];
|
|
161
|
+
const readValue = (flag) => {
|
|
162
|
+
const current = copy.shift();
|
|
163
|
+
if (current === flag) {
|
|
164
|
+
if (copy.length === 0) die(`${flag} requires a value`);
|
|
165
|
+
return String(copy.shift());
|
|
166
|
+
}
|
|
167
|
+
const prefix = `${flag}=`;
|
|
168
|
+
if (current && current.startsWith(prefix)) {
|
|
169
|
+
return current.slice(prefix.length);
|
|
170
|
+
}
|
|
171
|
+
copy.unshift(current);
|
|
172
|
+
return null;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
while (copy.length) {
|
|
176
|
+
const arg = copy[0];
|
|
177
|
+
if (arg === "--help" || arg === "-h") {
|
|
178
|
+
printProcessHelp();
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
if (arg === "--json") {
|
|
182
|
+
copy.shift();
|
|
183
|
+
out.json = true;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const state = readValue("--state");
|
|
187
|
+
if (state !== null) {
|
|
188
|
+
out.state = state;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const specialist = readValue("--specialist");
|
|
192
|
+
if (specialist !== null) {
|
|
193
|
+
out.specialist = specialist;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const cwd = readValue("--cwd");
|
|
197
|
+
if (cwd !== null) {
|
|
198
|
+
out.cwd = path.resolve(cwd);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const ticketJson = readValue("--ticket-json");
|
|
202
|
+
if (ticketJson !== null) {
|
|
203
|
+
out.ticketJson = ticketJson;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const ticketFile = readValue("--ticket-file");
|
|
207
|
+
if (ticketFile !== null) {
|
|
208
|
+
out.ticketFile = path.resolve(ticketFile);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const policiesFile = readValue("--policies-file");
|
|
212
|
+
if (policiesFile !== null) {
|
|
213
|
+
out.policiesFile = path.resolve(policiesFile);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
for (const [flag, key] of [
|
|
217
|
+
["--ticket-id", "id"],
|
|
218
|
+
["--ticket-key", "key"],
|
|
219
|
+
["--ticket-title", "title"],
|
|
220
|
+
["--ticket-url", "url"],
|
|
221
|
+
["--ticket-status", "status"],
|
|
222
|
+
["--ticket-description", "description"],
|
|
223
|
+
]) {
|
|
224
|
+
const value = readValue(flag);
|
|
225
|
+
if (value !== null) {
|
|
226
|
+
out.ticket[key] = value;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (copy[0] === arg) {
|
|
231
|
+
die(`unknown argument: ${arg}\nRun: shipctl process prompt --help`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!out.state && !out.specialist) {
|
|
235
|
+
die("either --state <state-id> or --specialist <specialist-id> is required");
|
|
236
|
+
}
|
|
237
|
+
if (out.state && out.specialist) {
|
|
238
|
+
die("use either --state or --specialist, not both");
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parseTicketsArgs(args) {
|
|
244
|
+
const out = {
|
|
245
|
+
workspace: null,
|
|
246
|
+
processId: "development",
|
|
247
|
+
tracker: null,
|
|
248
|
+
projectHint: null,
|
|
249
|
+
query: null,
|
|
250
|
+
state: "open",
|
|
251
|
+
limit: 10,
|
|
252
|
+
assigneeMe: false,
|
|
253
|
+
assignee: null,
|
|
254
|
+
baseUrl: null,
|
|
255
|
+
json: false,
|
|
256
|
+
};
|
|
257
|
+
const copy = [...args];
|
|
258
|
+
const readValue = (flag) => {
|
|
259
|
+
const current = copy.shift();
|
|
260
|
+
if (current === flag) {
|
|
261
|
+
if (copy.length === 0) die(`${flag} requires a value`);
|
|
262
|
+
return String(copy.shift());
|
|
263
|
+
}
|
|
264
|
+
const prefix = `${flag}=`;
|
|
265
|
+
if (current && current.startsWith(prefix)) {
|
|
266
|
+
return current.slice(prefix.length);
|
|
267
|
+
}
|
|
268
|
+
copy.unshift(current);
|
|
269
|
+
return null;
|
|
270
|
+
};
|
|
271
|
+
while (copy.length) {
|
|
272
|
+
const arg = copy[0];
|
|
273
|
+
if (arg === "--help" || arg === "-h") {
|
|
274
|
+
printProcessHelp();
|
|
275
|
+
process.exit(0);
|
|
276
|
+
}
|
|
277
|
+
if (arg === "--json") {
|
|
278
|
+
copy.shift();
|
|
279
|
+
out.json = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (arg === "--assignee-me") {
|
|
283
|
+
copy.shift();
|
|
284
|
+
out.assigneeMe = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
for (const [flag, key] of [
|
|
288
|
+
["--workspace", "workspace"],
|
|
289
|
+
["--process", "processId"],
|
|
290
|
+
["--tracker", "tracker"],
|
|
291
|
+
["--project-hint", "projectHint"],
|
|
292
|
+
["--query", "query"],
|
|
293
|
+
["--state", "state"],
|
|
294
|
+
["--limit", "limit"],
|
|
295
|
+
["--assignee", "assignee"],
|
|
296
|
+
["--base-url", "baseUrl"],
|
|
297
|
+
]) {
|
|
298
|
+
const value = readValue(flag);
|
|
299
|
+
if (value !== null) {
|
|
300
|
+
out[key] = key === "limit" ? Number(value) : value;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (copy[0] === arg) {
|
|
305
|
+
die(`unknown argument: ${arg}\nRun: shipctl process tickets --help`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!out.workspace) die("--workspace <id> is required");
|
|
309
|
+
if (!["open", "closed", "all"].includes(out.state)) {
|
|
310
|
+
die("--state must be one of open|closed|all");
|
|
311
|
+
}
|
|
312
|
+
if (!Number.isInteger(out.limit) || out.limit < 1 || out.limit > 25) {
|
|
313
|
+
die("--limit must be an integer between 1 and 25");
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function selectState(processConfig, opts) {
|
|
319
|
+
const states = Array.isArray(processConfig.states) ? processConfig.states : [];
|
|
320
|
+
if (opts.state) {
|
|
321
|
+
const state = states.find((item) => item.id === opts.state);
|
|
322
|
+
if (!state) die(`unknown process state ${JSON.stringify(opts.state)}`);
|
|
323
|
+
return state;
|
|
324
|
+
}
|
|
325
|
+
const state = states.find((item) => {
|
|
326
|
+
const specialist = item.specialist;
|
|
327
|
+
if (typeof specialist === "string") return specialist === opts.specialist;
|
|
328
|
+
if (specialist && typeof specialist === "object") {
|
|
329
|
+
return specialist.id === opts.specialist || specialist.name === opts.specialist;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
});
|
|
333
|
+
if (!state) die(`unknown process specialist ${JSON.stringify(opts.specialist)}`);
|
|
334
|
+
return state;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function resolveTicket(opts) {
|
|
338
|
+
const parts = [];
|
|
339
|
+
if (opts.ticketFile) {
|
|
340
|
+
const body = readOptionalFile(opts.ticketFile, "ticket");
|
|
341
|
+
if (body) {
|
|
342
|
+
parts.push(parseTicketJson(body, `ticket file ${opts.ticketFile}`));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (opts.ticketJson) {
|
|
346
|
+
parts.push(parseTicketJson(opts.ticketJson, "--ticket-json"));
|
|
347
|
+
}
|
|
348
|
+
if (Object.keys(opts.ticket).length) {
|
|
349
|
+
parts.push(opts.ticket);
|
|
350
|
+
}
|
|
351
|
+
if (!parts.length) return null;
|
|
352
|
+
return Object.assign({}, ...parts);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function parseTicketJson(value, label) {
|
|
356
|
+
try {
|
|
357
|
+
const parsed = JSON.parse(value);
|
|
358
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
359
|
+
die(`${label}: must be a JSON object`);
|
|
360
|
+
}
|
|
361
|
+
return parsed;
|
|
362
|
+
} catch (err) {
|
|
363
|
+
die(`${label}: failed to parse JSON (${err instanceof Error ? err.message : err})`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function readOptionalFile(filePath, label) {
|
|
368
|
+
if (!filePath) return null;
|
|
369
|
+
try {
|
|
370
|
+
return fs.readFileSync(filePath, "utf8");
|
|
371
|
+
} catch (err) {
|
|
372
|
+
die(`failed to read ${label} file ${filePath}: ${err instanceof Error ? err.message : err}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function resolveWorkspaceBaseUrl(explicit) {
|
|
377
|
+
return (
|
|
378
|
+
explicit ||
|
|
379
|
+
process.env.SHIP_WORKSPACE_API_BASE ||
|
|
380
|
+
process.env.SHIP_API_BASE ||
|
|
381
|
+
"https://api.ship.elmundi.com"
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function die(message) {
|
|
386
|
+
console.error(message);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|