@lightward/mechanic-cli 0.1.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +424 -0
  3. package/bin/mechanic.js +5 -0
  4. package/dist/auth.d.ts +10 -0
  5. package/dist/auth.d.ts.map +1 -0
  6. package/dist/auth.js +104 -0
  7. package/dist/base-command.d.ts +20 -0
  8. package/dist/base-command.d.ts.map +1 -0
  9. package/dist/base-command.js +82 -0
  10. package/dist/client.d.ts +40 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +172 -0
  13. package/dist/commands/auth/login.d.ts +10 -0
  14. package/dist/commands/auth/login.d.ts.map +1 -0
  15. package/dist/commands/auth/login.js +36 -0
  16. package/dist/commands/auth/logout.d.ts +6 -0
  17. package/dist/commands/auth/logout.d.ts.map +1 -0
  18. package/dist/commands/auth/logout.js +10 -0
  19. package/dist/commands/doctor.d.ts +7 -0
  20. package/dist/commands/doctor.d.ts.map +1 -0
  21. package/dist/commands/doctor.js +106 -0
  22. package/dist/commands/github/init.d.ts +10 -0
  23. package/dist/commands/github/init.d.ts.map +1 -0
  24. package/dist/commands/github/init.js +50 -0
  25. package/dist/commands/help.d.ts +7 -0
  26. package/dist/commands/help.d.ts.map +1 -0
  27. package/dist/commands/help.js +10 -0
  28. package/dist/commands/init.d.ts +13 -0
  29. package/dist/commands/init.d.ts.map +1 -0
  30. package/dist/commands/init.js +72 -0
  31. package/dist/commands/shop/status.d.ts +10 -0
  32. package/dist/commands/shop/status.d.ts.map +1 -0
  33. package/dist/commands/shop/status.js +138 -0
  34. package/dist/commands/tasks/bundle.d.ts +16 -0
  35. package/dist/commands/tasks/bundle.d.ts.map +1 -0
  36. package/dist/commands/tasks/bundle.js +83 -0
  37. package/dist/commands/tasks/diff.d.ts +16 -0
  38. package/dist/commands/tasks/diff.d.ts.map +1 -0
  39. package/dist/commands/tasks/diff.js +124 -0
  40. package/dist/commands/tasks/list.d.ts +11 -0
  41. package/dist/commands/tasks/list.d.ts.map +1 -0
  42. package/dist/commands/tasks/list.js +57 -0
  43. package/dist/commands/tasks/open.d.ts +13 -0
  44. package/dist/commands/tasks/open.d.ts.map +1 -0
  45. package/dist/commands/tasks/open.js +64 -0
  46. package/dist/commands/tasks/preview.d.ts +45 -0
  47. package/dist/commands/tasks/preview.d.ts.map +1 -0
  48. package/dist/commands/tasks/preview.js +373 -0
  49. package/dist/commands/tasks/publish.d.ts +16 -0
  50. package/dist/commands/tasks/publish.d.ts.map +1 -0
  51. package/dist/commands/tasks/publish.js +16 -0
  52. package/dist/commands/tasks/pull.d.ts +14 -0
  53. package/dist/commands/tasks/pull.d.ts.map +1 -0
  54. package/dist/commands/tasks/pull.js +96 -0
  55. package/dist/commands/tasks/push.d.ts +60 -0
  56. package/dist/commands/tasks/push.d.ts.map +1 -0
  57. package/dist/commands/tasks/push.js +370 -0
  58. package/dist/commands/tasks/status.d.ts +30 -0
  59. package/dist/commands/tasks/status.d.ts.map +1 -0
  60. package/dist/commands/tasks/status.js +183 -0
  61. package/dist/commands/tasks/unbundle.d.ts +16 -0
  62. package/dist/commands/tasks/unbundle.d.ts.map +1 -0
  63. package/dist/commands/tasks/unbundle.js +84 -0
  64. package/dist/commands/tasks/validate.d.ts +15 -0
  65. package/dist/commands/tasks/validate.d.ts.map +1 -0
  66. package/dist/commands/tasks/validate.js +78 -0
  67. package/dist/config.d.ts +15 -0
  68. package/dist/config.d.ts.map +1 -0
  69. package/dist/config.js +227 -0
  70. package/dist/errors.d.ts +10 -0
  71. package/dist/errors.d.ts.map +1 -0
  72. package/dist/errors.js +18 -0
  73. package/dist/fs.d.ts +10 -0
  74. package/dist/fs.d.ts.map +1 -0
  75. package/dist/fs.js +51 -0
  76. package/dist/github-workflows.d.ts +6 -0
  77. package/dist/github-workflows.d.ts.map +1 -0
  78. package/dist/github-workflows.js +293 -0
  79. package/dist/hash.d.ts +2 -0
  80. package/dist/hash.d.ts.map +1 -0
  81. package/dist/hash.js +5 -0
  82. package/dist/json.d.ts +4 -0
  83. package/dist/json.d.ts.map +1 -0
  84. package/dist/json.js +30 -0
  85. package/dist/tasks.d.ts +48 -0
  86. package/dist/tasks.d.ts.map +1 -0
  87. package/dist/tasks.js +546 -0
  88. package/dist/types.d.ts +144 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +1 -0
  91. package/package.json +80 -0
  92. package/schemas/mechanic.schema.json +13 -0
  93. package/schemas/task-config.schema.json +23 -0
@@ -0,0 +1,373 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../../base-command.js";
3
+ import { HttpError, CliError } from "../../errors.js";
4
+ import { pathExists } from "../../fs.js";
5
+ import { displayTaskPath, readRawTaskFile, resolveTaskSelector, rawTaskFromHelperDir, taskForPush, unbundledHelperDirForTaskFile, validateTaskForPush, } from "../../tasks.js";
6
+ function errorBodyAsPreview(body) {
7
+ if (!body || typeof body !== "object") {
8
+ return null;
9
+ }
10
+ const candidate = body;
11
+ return candidate.status === "invalid" ? candidate : null;
12
+ }
13
+ export default class TasksPreview extends BaseCommand {
14
+ static summary = "Preview one task with Mechanic sample events.";
15
+ static description = [
16
+ "Preview one local task JSON file or helper directory without publishing it.",
17
+ "The report shows sample event results, action failures, and Shopify permissions detected by the previewed paths.",
18
+ "Use --remote to preview the current task already in Mechanic, or --json for agents, scripts, and CI.",
19
+ ].join("\n");
20
+ static args = {
21
+ target: Args.string({ required: true, description: "Task file, helper directory, linked task ID, or unique local task slug." }),
22
+ };
23
+ static flags = {
24
+ json: Flags.boolean({ description: "Print the raw preview response as JSON for agents or CI." }),
25
+ remote: Flags.boolean({ description: "Preview the current task in Mechanic instead of local content." }),
26
+ verbose: Flags.boolean({ char: "v", description: "Show event, task run, and action run result details." }),
27
+ };
28
+ async run() {
29
+ const { args, flags } = await this.parse(TasksPreview);
30
+ const project = await this.loadProject();
31
+ const prepared = await this.preparePreview(project, args.target, Boolean(flags.remote));
32
+ const client = await this.verifiedClientForProject(project);
33
+ const response = await this.requestPreview(client, prepared, Boolean(flags.remote));
34
+ if (flags.json) {
35
+ this.outputJson(response);
36
+ this.exitForStatus(response);
37
+ return;
38
+ }
39
+ this.renderPreview(project, prepared, response, { verbose: Boolean(flags.verbose) });
40
+ this.exitForStatus(response);
41
+ }
42
+ exitForStatus(response) {
43
+ if (response.status === "invalid") {
44
+ throw new CliError("Preview validation failed.", 2);
45
+ }
46
+ if (response.status === "failed") {
47
+ throw new CliError("Preview found failed runs.", 1);
48
+ }
49
+ }
50
+ async preparePreview(project, target, remote) {
51
+ const selector = await resolveTaskSelector(project, target, {
52
+ allowHelperDir: true,
53
+ allowRemoteId: remote,
54
+ });
55
+ const label = selector.helperDir
56
+ ? displayTaskPath(project, selector.helperDir)
57
+ : selector.file ? displayTaskPath(project, selector.file) : target;
58
+ if (remote) {
59
+ if (!selector.remoteId) {
60
+ throw new CliError(`${label} is not linked to a remote task. Pull or publish it first.`, 2);
61
+ }
62
+ return {
63
+ label,
64
+ slug: selector.slug,
65
+ remoteId: selector.remoteId,
66
+ };
67
+ }
68
+ if (selector.helperDir) {
69
+ const task = await rawTaskFromHelperDir(selector.helperDir);
70
+ this.validateLocalTask(task, label);
71
+ return {
72
+ label,
73
+ slug: selector.slug,
74
+ task: taskForPush(task),
75
+ remoteId: selector.remoteId,
76
+ };
77
+ }
78
+ if (!selector.file) {
79
+ throw new CliError(`No local task content found for ${target}.`, 2);
80
+ }
81
+ if (!(await pathExists(selector.file))) {
82
+ throw new CliError([
83
+ `No local task file found for ${target}.`,
84
+ `Run "mechanic tasks pull ${selector.remoteId || target}" first, or use "mechanic tasks preview --remote ${selector.remoteId || target}" to preview the current task in Mechanic.`,
85
+ ].join("\n"), 2);
86
+ }
87
+ const task = await readRawTaskFile(selector.file);
88
+ this.validateLocalTask(task, label);
89
+ await this.blockStaleHelperDir(project, selector.file, label, task);
90
+ return {
91
+ label,
92
+ slug: selector.slug,
93
+ task: taskForPush(task),
94
+ remoteId: selector.remoteId,
95
+ };
96
+ }
97
+ async requestPreview(client, prepared, remote) {
98
+ try {
99
+ if (remote) {
100
+ if (!prepared.remoteId) {
101
+ throw new CliError(`${prepared.label} is not linked to a remote task.`, 2);
102
+ }
103
+ return await client.previewRemoteTask(prepared.remoteId);
104
+ }
105
+ if (!prepared.task) {
106
+ throw new CliError(`No local task content found for ${prepared.label}.`, 2);
107
+ }
108
+ return await client.previewTask(prepared.task, prepared.remoteId);
109
+ }
110
+ catch (error) {
111
+ if (error instanceof HttpError && error.status === 400) {
112
+ const preview = errorBodyAsPreview(error.body);
113
+ if (preview) {
114
+ return preview;
115
+ }
116
+ }
117
+ throw error;
118
+ }
119
+ }
120
+ renderPreview(project, prepared, response, options) {
121
+ this.log(`${this.statusLabel(response.status)} ${this.taskName(this.previewTitle(prepared, response))}`);
122
+ this.log(`Source: ${this.sourceSummary(project, prepared, response)}`);
123
+ this.log("");
124
+ this.table([
125
+ ["Events", "Task runs", "Action runs", "Failed"],
126
+ [
127
+ String(response.summary.events),
128
+ String(response.summary.task_runs),
129
+ String(response.summary.action_runs),
130
+ String(response.summary.failed_task_runs + response.summary.failed_action_runs),
131
+ ],
132
+ ]);
133
+ this.renderRunDetails(response);
134
+ if (options.verbose) {
135
+ this.renderVerboseDetails(response);
136
+ }
137
+ this.renderPermissions(response);
138
+ if (response.status === "invalid") {
139
+ this.renderValidationErrors(response);
140
+ }
141
+ if (response.permissions.approval.required) {
142
+ const appUrl = response.permissions.approval.app_url || project.appUrl;
143
+ this.log("");
144
+ this.log(`${this.accent("Approve in Mechanic:")} ${this.color("cyan", appUrl)}`);
145
+ }
146
+ }
147
+ renderVerboseDetails(response) {
148
+ if (response.events.length === 0) {
149
+ return;
150
+ }
151
+ this.log("");
152
+ this.log(this.accent("Preview details"));
153
+ for (const [eventIndex, event] of response.events.entries()) {
154
+ const eventLabel = response.events.length === 1
155
+ ? `Event: ${event.topic}`
156
+ : `Event ${eventIndex + 1}: ${event.topic}`;
157
+ this.log(` ${this.taskName(eventLabel)}`);
158
+ const eventDetails = [
159
+ event.source ? `source: ${event.source}` : null,
160
+ event.source_name ? `source name: ${event.source_name}` : null,
161
+ event.shopify_topic ? `Shopify topic: ${event.shopify_topic}` : null,
162
+ event.liquid_variable_names?.length ? `variables: ${event.liquid_variable_names.join(", ")}` : null,
163
+ ].filter(Boolean);
164
+ for (const detail of eventDetails) {
165
+ this.log(` ${this.muted(detail)}`);
166
+ }
167
+ if (event.task_runs.length === 0) {
168
+ this.log(` ${this.muted("No task runs.")}`);
169
+ continue;
170
+ }
171
+ for (const [taskRunIndex, taskRun] of event.task_runs.entries()) {
172
+ const taskRunLabel = event.task_runs.length === 1
173
+ ? "Task run"
174
+ : `Task run ${taskRunIndex + 1}`;
175
+ this.log(` ${taskRunLabel}: ${this.runStatusLabel(taskRun.ok)}`);
176
+ this.renderPreviewValue("error", taskRun.error, 6);
177
+ this.renderPreviewValue("result", taskRun.result, 6);
178
+ this.renderPreviewValue("meta", taskRun.result_meta, 6);
179
+ if (taskRun.action_runs.length === 0) {
180
+ this.log(` ${this.muted("No action runs.")}`);
181
+ continue;
182
+ }
183
+ for (const [actionRunIndex, actionRun] of taskRun.action_runs.entries()) {
184
+ const actionLabel = actionRun.action_type || `action ${actionRunIndex + 1}`;
185
+ this.log(` Action ${actionRunIndex + 1} ${this.taskName(actionLabel)}: ${this.runStatusLabel(actionRun.ok)}`);
186
+ this.renderPreviewValue("error", actionRun.error, 8);
187
+ this.renderPreviewValue("result", actionRun.result, 8);
188
+ this.renderPreviewValue("meta", actionRun.result_meta, 8);
189
+ }
190
+ }
191
+ }
192
+ }
193
+ renderRunDetails(response) {
194
+ const rows = [["Event", "Task", "Actions", "Details"]];
195
+ for (const event of response.events) {
196
+ for (const taskRun of event.task_runs) {
197
+ const actionSummary = taskRun.action_runs.length === 0
198
+ ? "none"
199
+ : taskRun.action_runs.map((actionRun) => {
200
+ const name = actionRun.action_type || "action";
201
+ return actionRun.ok === false ? `${name} failed` : name;
202
+ }).join(", ");
203
+ const details = [
204
+ taskRun.error,
205
+ ...taskRun.action_runs.flatMap((actionRun) => actionRun.error ? [`${actionRun.action_type || "action"}: ${actionRun.error}`] : []),
206
+ ].filter(Boolean).join("; ");
207
+ rows.push([
208
+ this.taskName(event.topic),
209
+ taskRun.ok === false ? this.color("red", "failed") : this.success("passed"),
210
+ actionSummary,
211
+ details,
212
+ ]);
213
+ }
214
+ }
215
+ if (rows.length > 1) {
216
+ this.log("");
217
+ this.table(rows);
218
+ }
219
+ }
220
+ renderPreviewValue(label, value, indent) {
221
+ const formatted = this.formatPreviewValue(value);
222
+ if (!formatted) {
223
+ return;
224
+ }
225
+ const prefix = " ".repeat(indent);
226
+ const continuationPrefix = " ".repeat(indent + label.length + 2);
227
+ const lines = formatted.split("\n");
228
+ this.log(`${prefix}${this.muted(`${label}:`)} ${lines[0]}`);
229
+ for (const line of lines.slice(1)) {
230
+ this.log(`${continuationPrefix}${line}`);
231
+ }
232
+ }
233
+ formatPreviewValue(value) {
234
+ if (value === null || value === undefined || value === "") {
235
+ return null;
236
+ }
237
+ const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
238
+ const maxLength = 600;
239
+ const normalized = text.trimEnd();
240
+ if (normalized.length <= maxLength) {
241
+ return normalized;
242
+ }
243
+ return `${normalized.slice(0, maxLength)}... ${this.muted("(truncated; use --json for full response)")}`;
244
+ }
245
+ runStatusLabel(ok) {
246
+ if (ok === true) {
247
+ return this.success("passed");
248
+ }
249
+ if (ok === false) {
250
+ return this.color("red", "failed");
251
+ }
252
+ return this.muted("unknown");
253
+ }
254
+ renderPermissions(response) {
255
+ const permissions = response.permissions;
256
+ this.log("");
257
+ this.log(this.accent("Shopify permissions"));
258
+ if (permissions.required_by_preview.length === 0) {
259
+ this.log(` ${this.success("No Shopify Admin API permissions detected in this preview.")}`);
260
+ return;
261
+ }
262
+ this.log(` Required in this preview: ${permissions.required_by_preview.join(", ")}`);
263
+ if (permissions.missing.app.length === 0 && permissions.missing.task_runtime.length === 0) {
264
+ this.log(` ${this.success("Already approved for this shop.")}`);
265
+ }
266
+ else {
267
+ this.log(` Missing approvals: ${this.color("yellow", this.uniqueScopes([
268
+ ...permissions.missing.app,
269
+ ...permissions.missing.task_runtime,
270
+ ]).join(", "))}`);
271
+ this.log(` ${this.muted(this.approvalHint(permissions.approval.action))}`);
272
+ }
273
+ for (const warning of permissions.warnings) {
274
+ this.log(` ${this.muted(`Note: ${warning}`)}`);
275
+ }
276
+ }
277
+ renderValidationErrors(response) {
278
+ this.log("");
279
+ this.log(this.color("red", "Validation errors"));
280
+ if (response.message) {
281
+ this.log(` ${response.message}`);
282
+ }
283
+ for (const [field, messages] of Object.entries(response.errors)) {
284
+ if (Array.isArray(messages)) {
285
+ for (const message of messages) {
286
+ this.log(` ${field}: ${message}`);
287
+ }
288
+ }
289
+ else {
290
+ this.log(` ${field}: ${String(messages)}`);
291
+ }
292
+ }
293
+ }
294
+ previewTitle(prepared, response) {
295
+ return response.task.name || prepared.label;
296
+ }
297
+ sourceSummary(project, prepared, response) {
298
+ const taskId = response.source.task_id || prepared.remoteId;
299
+ const taskIdLabel = taskId ? this.taskId(project, taskId) : "new task";
300
+ const enabled = response.source.remote_enabled === true
301
+ ? "enabled"
302
+ : response.source.remote_enabled === false
303
+ ? "disabled"
304
+ : null;
305
+ switch (response.source.kind) {
306
+ case "remote_current":
307
+ return [
308
+ this.taskName("current task in Mechanic"),
309
+ taskIdLabel,
310
+ enabled ? this.muted(enabled) : null,
311
+ ].filter(Boolean).join(" ");
312
+ case "local_draft_for_existing_task":
313
+ return [
314
+ this.taskName(prepared.label),
315
+ this.muted("previewing against"),
316
+ taskIdLabel,
317
+ enabled ? this.muted(`currently ${enabled}`) : null,
318
+ ].filter(Boolean).join(" ");
319
+ case "local_draft":
320
+ return [
321
+ this.taskName(prepared.label),
322
+ this.muted("not published yet"),
323
+ ].join(" ");
324
+ }
325
+ }
326
+ uniqueScopes(scopes) {
327
+ return [...new Set(scopes)].sort();
328
+ }
329
+ validateLocalTask(task, label) {
330
+ const validation = validateTaskForPush(task, label);
331
+ if (!validation.ok) {
332
+ throw new CliError([
333
+ `${label} is not a valid task export.`,
334
+ ...validation.errors.map((error) => `- ${error}`),
335
+ ].join("\n"), 2);
336
+ }
337
+ }
338
+ async blockStaleHelperDir(project, filePath, relativeFile, task) {
339
+ const helperDir = await unbundledHelperDirForTaskFile(filePath, task);
340
+ if (!helperDir) {
341
+ return;
342
+ }
343
+ const relativeHelperDir = displayTaskPath(project, helperDir);
344
+ throw new CliError([
345
+ `${relativeFile} is out of date with ${relativeHelperDir}.`,
346
+ "",
347
+ "Bundle the helper directory before previewing this JSON file:",
348
+ ` mechanic tasks bundle ${relativeHelperDir}`,
349
+ ].join("\n"), 2);
350
+ }
351
+ approvalHint(action) {
352
+ switch (action) {
353
+ case "open_app_permissions":
354
+ return "Open Mechanic to approve the missing permissions.";
355
+ case "enable_task_in_app":
356
+ return "Enable the task in Mechanic to request the missing permissions.";
357
+ case "publish_task_then_open_app_permissions":
358
+ return "Publish this task, then open Mechanic to approve the missing permissions.";
359
+ default:
360
+ return "Open Mechanic if this task needs additional Shopify permissions.";
361
+ }
362
+ }
363
+ statusLabel(status) {
364
+ switch (status) {
365
+ case "passed":
366
+ return this.success("Preview passed");
367
+ case "failed":
368
+ return this.color("red", "Preview failed");
369
+ case "invalid":
370
+ return this.color("red", "Preview invalid");
371
+ }
372
+ }
373
+ }
@@ -0,0 +1,16 @@
1
+ import TasksPush from "./push.js";
2
+ export default class TasksPublish extends TasksPush {
3
+ static hidden: boolean;
4
+ static summary: string;
5
+ static description: string;
6
+ static args: {
7
+ file: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ "dry-run": import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ }
16
+ //# sourceMappingURL=publish.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/publish.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,SAAS;IACjD,OAAgB,MAAM,UAAS;IAC/B,OAAgB,OAAO,SAAyC;IAChE,OAAgB,WAAW,SAA6F;IAExH,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;;;MAKnB;CACH"}
@@ -0,0 +1,16 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import TasksPush from "./push.js";
3
+ export default class TasksPublish extends TasksPush {
4
+ static hidden = false;
5
+ static summary = "Publish one local task to Mechanic.";
6
+ static description = "Publish one local task to Mechanic, or use --all to publish every local task JSON file.";
7
+ static args = {
8
+ file: Args.string({ required: false, description: "Task file, helper directory, linked task ID, or unique local task slug." }),
9
+ };
10
+ static flags = {
11
+ all: Flags.boolean({ description: "Publish every task JSON file in this project." }),
12
+ "dry-run": Flags.boolean({ description: "Show what would be published without writing to Mechanic or local files." }),
13
+ force: Flags.boolean({ description: "Bypass remote content hash conflict protection." }),
14
+ json: Flags.boolean({ description: "Print publish result or dry-run plan as JSON for agents, scripts, or editor integrations." }),
15
+ };
16
+ }
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class TasksPull extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static args: {
6
+ task: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
14
+ //# sourceMappingURL=pull.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/pull.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAkBpD,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAChD,OAAgB,OAAO,SAAmD;IAC1E,OAAgB,WAAW,SAAmI;IAE9J,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;MAGnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAwF3B"}
@@ -0,0 +1,96 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import path from "node:path";
3
+ import { BaseCommand } from "../../base-command.js";
4
+ import { saveLinks, slugForRemoteId, updateLink } from "../../config.js";
5
+ import { pathExists } from "../../fs.js";
6
+ import { contentHash } from "../../hash.js";
7
+ import { CliError } from "../../errors.js";
8
+ import { displayTaskPath, nextAvailableSlug, readRawTaskFile, resolveTaskSelector, taskForPush, taskPath, taskSlug, unbundledHelperDirForTaskFile, writeTaskFilePathAndRefreshHelper, } from "../../tasks.js";
9
+ export default class TasksPull extends BaseCommand {
10
+ static summary = "Pull remote tasks into local task JSON files.";
11
+ static description = "Pull every remote task into local canonical task JSON files, or pass a task ID, linked file, or linked slug to pull one task.";
12
+ static args = {
13
+ task: Args.string({ required: false, description: "Remote task ID, linked local task JSON file, or linked local task slug." }),
14
+ };
15
+ static flags = {
16
+ all: Flags.boolean({ description: "Pull every remote task for this shop. This is the default when no task ID is passed." }),
17
+ force: Flags.boolean({ description: "Overwrite existing local task files." }),
18
+ };
19
+ async run() {
20
+ const { args, flags } = await this.parse(TasksPull);
21
+ if (args.task && flags.all) {
22
+ throw new CliError("tasks pull accepts either <task-id> or --all.");
23
+ }
24
+ const project = await this.loadProject();
25
+ const client = await this.verifiedClientForProject(project);
26
+ let links = project.links;
27
+ const rows = [["Task ID", "Slug", "Hash"]];
28
+ const pullEnvelope = async (envelope) => {
29
+ const linkedSlug = slugForRemoteId({ ...project, links }, envelope.id);
30
+ const slug = linkedSlug || await nextAvailableSlug(project, taskSlug(String(envelope.task.name || envelope.id)));
31
+ const link = linkedSlug ? links.tasks[linkedSlug] : null;
32
+ const filePath = link?.file
33
+ ? path.resolve(project.cwd, link.file)
34
+ : taskPath(project, slug);
35
+ if (await pathExists(filePath)) {
36
+ if (!linkedSlug) {
37
+ if (!flags.force) {
38
+ throw new CliError(`${displayTaskPath(project, filePath)} already exists. Re-run with --force to overwrite.`);
39
+ }
40
+ }
41
+ else if (!flags.force) {
42
+ const localTask = await readRawTaskFile(filePath);
43
+ const helperDir = await unbundledHelperDirForTaskFile(filePath, localTask);
44
+ if (helperDir) {
45
+ const relativeFile = displayTaskPath(project, filePath);
46
+ const relativeHelperDir = displayTaskPath(project, helperDir);
47
+ throw new CliError([
48
+ `${relativeFile} is out of date with ${relativeHelperDir}.`,
49
+ "",
50
+ "The helper directory has changes that are not bundled into the JSON file.",
51
+ "",
52
+ "Bundle first:",
53
+ ` mechanic tasks bundle ${relativeHelperDir}`,
54
+ "",
55
+ "Then pull:",
56
+ ` mechanic tasks pull ${envelope.id}`,
57
+ ].join("\n"), 2);
58
+ }
59
+ const localHash = contentHash(taskForPush(localTask));
60
+ if (localHash !== link?.last_remote_content_hash) {
61
+ throw new CliError([
62
+ `${displayTaskPath(project, filePath)} has local changes that would be overwritten.`,
63
+ `Run "mechanic tasks diff ${displayTaskPath(project, filePath)}" to review them.`,
64
+ `Re-run "mechanic tasks pull ${envelope.id} --force" only if the remote version should replace the local file.`,
65
+ ].join("\n"), 2);
66
+ }
67
+ }
68
+ }
69
+ await writeTaskFilePathAndRefreshHelper(filePath, envelope.task);
70
+ links = updateLink({ ...project, links }, slug, envelope.id, envelope.content_hash);
71
+ await saveLinks(project, links);
72
+ rows.push([
73
+ this.taskId(project, envelope.id),
74
+ this.taskName(slug),
75
+ this.muted(envelope.content_hash),
76
+ ]);
77
+ };
78
+ if (flags.all || !args.task) {
79
+ const response = await client.listTasks();
80
+ for (const summary of response.tasks || []) {
81
+ await pullEnvelope(summary.task ? summary : await client.getTask(summary.id));
82
+ }
83
+ }
84
+ else if (args.task) {
85
+ const selector = await resolveTaskSelector(project, args.task, {
86
+ allowHelperDir: true,
87
+ allowRemoteId: true,
88
+ });
89
+ if (!selector.remoteId) {
90
+ throw new CliError(`${args.task} is not linked to a remote task. Pass a remote task ID or pull all tasks.`, 2);
91
+ }
92
+ await pullEnvelope(await client.getTask(selector.remoteId));
93
+ }
94
+ this.table(rows);
95
+ }
96
+ }
@@ -0,0 +1,60 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ import type { MechanicClient } from "../../client.js";
3
+ import type { JsonObject, LinksFile, Project, TaskEnvelope } from "../../types.js";
4
+ type PreparedTask = {
5
+ file: string;
6
+ slug: string;
7
+ relativeFile: string;
8
+ task: JsonObject;
9
+ };
10
+ type PrepareResult = {
11
+ blocked: boolean;
12
+ preparedTasks: PreparedTask[];
13
+ rows: PushRow[];
14
+ };
15
+ type PushRow = {
16
+ file: string;
17
+ status: string;
18
+ task_id: string | null;
19
+ content_hash: string | null;
20
+ details: string;
21
+ };
22
+ type CreateCollision = {
23
+ remoteId: string;
24
+ remoteName: string;
25
+ };
26
+ type TasksPushFlags = {
27
+ all?: boolean;
28
+ "dry-run"?: boolean;
29
+ force?: boolean;
30
+ json?: boolean;
31
+ };
32
+ export default class TasksPush extends BaseCommand {
33
+ static hidden: boolean;
34
+ static summary: string;
35
+ static description: string;
36
+ static args: {
37
+ file: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
38
+ };
39
+ static flags: {
40
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
41
+ "dry-run": import("@oclif/core/interfaces").BooleanFlag<boolean>;
42
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
43
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
44
+ };
45
+ run(): Promise<void>;
46
+ runPush(file: string | undefined, flags: TasksPushFlags): Promise<void>;
47
+ publishTasks(project: Project, preparation: PrepareResult, force: boolean): Promise<PushRow[]>;
48
+ preflightRemoteChanges(client: MechanicClient, project: Project, links: LinksFile, preparedTasks: PreparedTask[]): Promise<Map<string, TaskEnvelope>>;
49
+ unlinkedCreateCollisions(client: MechanicClient, project: Project, links: LinksFile, preparedTasks: PreparedTask[]): Promise<Map<string, CreateCollision>>;
50
+ blockUnlinkedCreateCollisions(client: MechanicClient, project: Project, links: LinksFile, preparedTasks: PreparedTask[]): Promise<void>;
51
+ prepareTasks(project: Project, files: string[], collectBlockedRows: boolean): Promise<PrepareResult>;
52
+ dryRunPush(project: Project, preparation: PrepareResult, force: boolean): Promise<{
53
+ blocked: boolean;
54
+ rows: PushRow[];
55
+ }>;
56
+ pushRowsToTable(project: Project, statusHeader: string, rows: PushRow[], label: (status: string) => string): string[][];
57
+ planLabel(plan: string): string;
58
+ }
59
+ export {};
60
+ //# sourceMappingURL=push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/push.ts"],"names":[],"mappings":"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"}