@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,138 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../../base-command.js";
3
+ function plural(value, singular, pluralized = `${singular}s`) {
4
+ return value === 1 ? singular : pluralized;
5
+ }
6
+ function formatCount(value) {
7
+ return typeof value === "number" ? value.toLocaleString() : "--";
8
+ }
9
+ function formatLag(milliseconds) {
10
+ if (milliseconds === null || milliseconds === undefined) {
11
+ return "--";
12
+ }
13
+ if (milliseconds <= 0) {
14
+ return "none";
15
+ }
16
+ const totalSeconds = Math.round(milliseconds / 1000);
17
+ if (totalSeconds < 60) {
18
+ return `${totalSeconds}s`;
19
+ }
20
+ const totalMinutes = Math.floor(totalSeconds / 60);
21
+ const seconds = totalSeconds % 60;
22
+ if (totalMinutes < 60) {
23
+ return seconds ? `${totalMinutes}m ${seconds}s` : `${totalMinutes}m`;
24
+ }
25
+ const hours = Math.floor(totalMinutes / 60);
26
+ const minutes = totalMinutes % 60;
27
+ return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
28
+ }
29
+ function queueSummary(status) {
30
+ const running = status.queue.running ?? 0;
31
+ const limit = status.queue.limit;
32
+ const waiting = status.queue.waiting ?? 0;
33
+ const runningText = limit === null || limit === undefined
34
+ ? `${formatCount(running)} running`
35
+ : `${formatCount(running)}/${formatCount(limit)} running`;
36
+ return `${runningText}, ${formatCount(waiting)} ${plural(waiting, "run")} waiting`;
37
+ }
38
+ export default class ShopStatus extends BaseCommand {
39
+ static summary = "Show current Mechanic queue and backlog for this shop.";
40
+ static description = "Show how many runs are running or waiting, plus the largest current backlog groups.";
41
+ static flags = {
42
+ json: Flags.boolean({
43
+ description: "Print shop status as JSON for agents, scripts, or editor integrations.",
44
+ }),
45
+ };
46
+ async run() {
47
+ const { flags } = await this.parse(ShopStatus);
48
+ const project = await this.loadProject();
49
+ const client = await this.verifiedClientForProject(project);
50
+ const status = await client.getShopStatus();
51
+ const waiting = status.queue.waiting ?? 0;
52
+ if (flags.json) {
53
+ this.outputJson({
54
+ ...status,
55
+ queue_summary: queueSummary(status),
56
+ formatted_lag: {
57
+ queue: formatLag(status.queue.oldest_waiting_ms),
58
+ event_runs: formatLag(status.lag.event_run_ms),
59
+ task_runs: formatLag(status.lag.task_run_ms),
60
+ action_runs: formatLag(status.lag.action_run_ms),
61
+ },
62
+ });
63
+ return;
64
+ }
65
+ this.log(`${this.accent("Shop")} ${status.shop.shopify_domain}`);
66
+ this.log(`${this.accent("Queue")} ${queueSummary(status)}`);
67
+ this.log(`${this.accent("Queue lag")} ${formatLag(status.queue.oldest_waiting_ms)}`);
68
+ if (status.queue.partial) {
69
+ this.warn("Mechanic could not collect every queue statistic before the database timeout. Try again in a moment.");
70
+ }
71
+ this.log("");
72
+ this.table([
73
+ ["State", "Event runs", "Task runs", "Action runs"],
74
+ [
75
+ "Running",
76
+ formatCount(status.running.event_runs),
77
+ formatCount(status.running.task_runs),
78
+ formatCount(status.running.action_runs),
79
+ ],
80
+ [
81
+ "Waiting",
82
+ formatCount(status.backlog.event_runs),
83
+ formatCount(status.backlog.task_runs),
84
+ formatCount(status.backlog.action_runs),
85
+ ],
86
+ ]);
87
+ if (waiting === 0) {
88
+ this.log("");
89
+ this.log(this.success("No runs waiting."));
90
+ return;
91
+ }
92
+ this.log("");
93
+ this.log(this.accent("Lag by type"));
94
+ this.table([
95
+ ["Run type", "Lag"],
96
+ ["Event runs", formatLag(status.lag.event_run_ms)],
97
+ ["Task runs", formatLag(status.lag.task_run_ms)],
98
+ ["Action runs", formatLag(status.lag.action_run_ms)],
99
+ ]);
100
+ this.log("");
101
+ this.log(this.accent("Top waiting tasks"));
102
+ if (status.backlog.by_task.length === 0) {
103
+ this.log(this.muted("No task-run backlog."));
104
+ }
105
+ else {
106
+ this.table([
107
+ ["Task", "Waiting"],
108
+ ...status.backlog.by_task.map((row) => [
109
+ this.taskName(row.task_name || row.task_id),
110
+ formatCount(row.count),
111
+ ]),
112
+ ]);
113
+ }
114
+ if (status.backlog.by_action.length > 0) {
115
+ this.log("");
116
+ this.log(this.accent("Top waiting actions"));
117
+ this.table([
118
+ ["Action", "Task", "Waiting"],
119
+ ...status.backlog.by_action.map((row) => [
120
+ row.action_type || "(unknown)",
121
+ this.taskName(row.task_name || row.task_id),
122
+ formatCount(row.count),
123
+ ]),
124
+ ]);
125
+ }
126
+ if (status.backlog.by_event_topic.length > 0) {
127
+ this.log("");
128
+ this.log(this.accent("Top waiting events"));
129
+ this.table([
130
+ ["Event topic", "Waiting"],
131
+ ...status.backlog.by_event_topic.map((row) => [
132
+ row.event_topic || "(unknown)",
133
+ formatCount(row.count),
134
+ ]),
135
+ ]);
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class TasksBundle extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ dir: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ out: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ private resolveBundlePaths;
15
+ }
16
+ //# sourceMappingURL=bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/bundle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAKpD,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,WAAW;IAClD,OAAgB,OAAO,SAA+D;IACtF,OAAgB,WAAW,SAAkF;IAC7G,OAAgB,QAAQ,WAItB;IAEF,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;MAOnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YAgCZ,kBAAkB;CAuCjC"}
@@ -0,0 +1,83 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import path from "node:path";
3
+ import { BaseCommand } from "../../base-command.js";
4
+ import { CliError } from "../../errors.js";
5
+ import { pathExists } from "../../fs.js";
6
+ import { bundleTask } from "../../tasks.js";
7
+ export default class TasksBundle extends BaseCommand {
8
+ static summary = "Bundle one helper task directory into one task JSON file.";
9
+ static description = "Bundle one editable helper task directory into one canonical task JSON file.";
10
+ static examples = [
11
+ "$ mechanic tasks bundle tasks/order-tagger",
12
+ "$ mechanic tasks bundle tasks/order-tagger.json",
13
+ "$ mechanic tasks bundle tasks/order-tagger --out tasks/order-tagger.json",
14
+ ];
15
+ static args = {
16
+ dir: Args.string({ required: false, description: "Helper task directory, or its matching task JSON file." }),
17
+ };
18
+ static flags = {
19
+ json: Flags.boolean({
20
+ description: "Print bundle result as JSON for agents, scripts, or editor integrations.",
21
+ }),
22
+ out: Flags.string({
23
+ description: "Output canonical task JSON file. Defaults to the helper directory path plus .json.",
24
+ }),
25
+ };
26
+ async run() {
27
+ const { args, flags } = await this.parse(TasksBundle);
28
+ if (!args.dir) {
29
+ throw new CliError([
30
+ "Choose one helper task directory to bundle.",
31
+ "",
32
+ "Example:",
33
+ " mechanic tasks bundle tasks/order-tagger",
34
+ "",
35
+ "By default this writes to tasks/order-tagger.json. Use --out <file> to choose another task JSON file.",
36
+ ].join("\n"), 2);
37
+ }
38
+ const { dirPath, inputDisplay, outputDisplay } = await this.resolveBundlePaths(args.dir, flags.out);
39
+ const outputPath = path.resolve(outputDisplay);
40
+ await bundleTask(dirPath, outputPath);
41
+ if (flags.json) {
42
+ this.outputJson({
43
+ ok: true,
44
+ input: inputDisplay,
45
+ input_path: dirPath,
46
+ output: outputDisplay,
47
+ output_path: outputPath,
48
+ });
49
+ return;
50
+ }
51
+ this.log(`Bundled ${this.taskName(inputDisplay)} -> ${this.taskName(outputDisplay)}`);
52
+ }
53
+ async resolveBundlePaths(input, out) {
54
+ const inputPath = path.resolve(input);
55
+ if (await pathExists(path.join(inputPath, "task.json"))) {
56
+ return {
57
+ dirPath: inputPath,
58
+ inputDisplay: input,
59
+ outputDisplay: out || `${input}.json`,
60
+ };
61
+ }
62
+ if (!input.toLowerCase().endsWith(".json")) {
63
+ return {
64
+ dirPath: inputPath,
65
+ inputDisplay: input,
66
+ outputDisplay: out || `${input}.json`,
67
+ };
68
+ }
69
+ const helperInput = input.slice(0, -".json".length);
70
+ const helperPath = path.resolve(helperInput);
71
+ if (!(await pathExists(path.join(helperPath, "task.json")))) {
72
+ throw new CliError([
73
+ `No helper task directory found for ${this.taskName(input)}.`,
74
+ `Run "mechanic tasks unbundle ${this.taskName(input)}" first, then bundle again after editing.`,
75
+ ].join("\n"), 2);
76
+ }
77
+ return {
78
+ dirPath: helperPath,
79
+ inputDisplay: helperInput,
80
+ outputDisplay: out || input,
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class TasksDiff extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static args: {
6
+ file: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ "exit-code": import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ colorDiff(diff: string): string;
14
+ run(): Promise<void>;
15
+ }
16
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/diff.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAYpD,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAChD,OAAgB,OAAO,SAA2D;IAClF,OAAgB,WAAW,SAA6G;IAExI,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;;MAMnB;IAEF,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IA2BzB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAmH3B"}
@@ -0,0 +1,124 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../../base-command.js";
3
+ import { linkForSlug } from "../../config.js";
4
+ import { diffTasks, displayTaskPath, readTaskFile, remoteChangedSinceLastPull, selectedTaskFiles, slugFromTaskFile, taskForPush, } from "../../tasks.js";
5
+ export default class TasksDiff extends BaseCommand {
6
+ static summary = "Diff one task, or use --all to diff every local task.";
7
+ static description = "Compare one local task with its linked remote task, or use --all to compare every local task JSON file.";
8
+ static args = {
9
+ file: Args.string({ required: false, description: "Task file, helper directory, linked task ID, or unique local task slug." }),
10
+ };
11
+ static flags = {
12
+ all: Flags.boolean({ description: "Diff every task JSON file in this project." }),
13
+ "exit-code": Flags.boolean({ description: "Exit 1 when differences are found. Useful for CI." }),
14
+ json: Flags.boolean({
15
+ description: "Print diff results as JSON for agents, scripts, or editor integrations.",
16
+ }),
17
+ };
18
+ colorDiff(diff) {
19
+ return diff.split("\n").map((line) => {
20
+ if (line.startsWith("+") && !line.startsWith("+++")) {
21
+ return this.success(line);
22
+ }
23
+ if (line.startsWith("-") && !line.startsWith("---")) {
24
+ return this.color("red", line);
25
+ }
26
+ if (line.startsWith("#")) {
27
+ return this.accent(line);
28
+ }
29
+ if (line.startsWith("@@")
30
+ || line.startsWith("---")
31
+ || line.startsWith("+++")
32
+ || line.startsWith("Changed fields:")) {
33
+ return this.muted(line);
34
+ }
35
+ return line;
36
+ }).join("\n");
37
+ }
38
+ async run() {
39
+ const { args, flags } = await this.parse(TasksDiff);
40
+ const project = await this.loadProject();
41
+ const client = await this.verifiedClientForProject(project);
42
+ const files = await selectedTaskFiles(project, args.file, Boolean(flags.all));
43
+ const differences = [];
44
+ const results = [];
45
+ let hasFieldDifferences = false;
46
+ let hasRemoteChanges = false;
47
+ for (const file of files) {
48
+ const slug = slugFromTaskFile(file);
49
+ const link = linkForSlug(project, slug);
50
+ const relativeFile = displayTaskPath(project, file);
51
+ if (!link) {
52
+ results.push({
53
+ file: relativeFile,
54
+ slug,
55
+ linked: false,
56
+ remote_id: null,
57
+ remote_changed: false,
58
+ field_differences: false,
59
+ diff: null,
60
+ message: "Task is not linked to a remote task.",
61
+ });
62
+ differences.push(`# ${relativeFile}\nTask is not linked to a remote task.`);
63
+ continue;
64
+ }
65
+ const localTask = taskForPush(await readTaskFile(file));
66
+ const remote = await client.getTask(link.remote_id);
67
+ const remoteTask = taskForPush(remote.task);
68
+ const diff = diffTasks(localTask, remoteTask);
69
+ const remoteChanged = remoteChangedSinceLastPull(link, remote);
70
+ const hasDiff = Boolean(diff);
71
+ if (remoteChanged) {
72
+ hasRemoteChanges = true;
73
+ }
74
+ if (hasDiff) {
75
+ hasFieldDifferences = true;
76
+ }
77
+ results.push({
78
+ file: relativeFile,
79
+ slug,
80
+ linked: true,
81
+ remote_id: link.remote_id,
82
+ remote_changed: remoteChanged,
83
+ field_differences: hasDiff,
84
+ diff,
85
+ });
86
+ if (remoteChanged || diff) {
87
+ const section = [`# ${relativeFile}`];
88
+ if (remoteChanged) {
89
+ section.push(this.color("yellow", "Remote changed since your last pull."), this.muted("Showing the current Mechanic task compared with your local file."), this.muted(`Run "mechanic tasks pull ${link.remote_id}" before publishing, or use --force only if the local file should win.`), "");
90
+ }
91
+ if (diff) {
92
+ section.push(diff);
93
+ }
94
+ else {
95
+ section.push("No field differences between the current remote task and your local file.");
96
+ }
97
+ differences.push(this.colorDiff(section.join("\n")));
98
+ }
99
+ }
100
+ if (flags.json) {
101
+ const differencesFound = results.some((result) => result.remote_changed || result.field_differences || !result.linked);
102
+ this.outputJson({
103
+ shop_domain: project.shopDomain,
104
+ differences_found: differencesFound,
105
+ field_differences_found: hasFieldDifferences,
106
+ remote_changes_found: hasRemoteChanges,
107
+ tasks: results,
108
+ });
109
+ if (flags["exit-code"] && differencesFound) {
110
+ process.exitCode = 1;
111
+ }
112
+ return;
113
+ }
114
+ if (differences.length === 0) {
115
+ this.log("No differences.");
116
+ return;
117
+ }
118
+ this.log(differences.join("\n\n"));
119
+ this.log(hasFieldDifferences ? "Differences found." : "Remote changes found.");
120
+ if (flags["exit-code"]) {
121
+ process.exitCode = 1;
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,11 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class TasksList extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
11
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/list.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGpD,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAChD,OAAgB,OAAO,SAAkD;IACzE,OAAgB,WAAW,SAAsF;IAEjH,OAAgB,KAAK;;;MAQnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAgD3B"}
@@ -0,0 +1,57 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../../base-command.js";
3
+ import { slugForRemoteId } from "../../config.js";
4
+ export default class TasksList extends BaseCommand {
5
+ static summary = "List remote tasks without changing anything.";
6
+ static description = "List remote Mechanic tasks without pulling, publishing, or changing local files.";
7
+ static flags = {
8
+ json: Flags.boolean({
9
+ description: "Print task list as JSON for agents, scripts, or editor integrations.",
10
+ }),
11
+ verbose: Flags.boolean({
12
+ char: "v",
13
+ description: "Show sync hashes.",
14
+ }),
15
+ };
16
+ async run() {
17
+ const { flags } = await this.parse(TasksList);
18
+ const project = await this.loadProject();
19
+ const client = await this.verifiedClientForProject(project);
20
+ const response = await client.listTasks();
21
+ if (flags.json) {
22
+ this.outputJson({
23
+ shop_domain: project.shopDomain,
24
+ tasks: (response.tasks || []).map((envelope) => {
25
+ const slug = slugForRemoteId(project, envelope.id);
26
+ const link = slug ? project.links.tasks[slug] : null;
27
+ return {
28
+ id: envelope.id,
29
+ name: typeof envelope.task?.name === "string" ? envelope.task.name : "",
30
+ updated_at: envelope.updated_at || null,
31
+ content_hash: envelope.content_hash || null,
32
+ linked: Boolean(link),
33
+ slug: slug || null,
34
+ file: link ? link.file || `${project.tasksDirName}/${slug}.json` : null,
35
+ };
36
+ }),
37
+ });
38
+ return;
39
+ }
40
+ const rows = flags.verbose ? [["Name", "File", "Updated", "ID", "Hash"]] : [["Name", "File", "Updated", "ID"]];
41
+ for (const envelope of response.tasks || []) {
42
+ const slug = slugForRemoteId(project, envelope.id);
43
+ const link = slug ? project.links.tasks[slug] : null;
44
+ const row = [
45
+ this.taskName(String(envelope.task?.name || "")),
46
+ link ? this.taskName(link.file || `${project.tasksDirName}/${slug}.json`) : this.muted("--"),
47
+ this.muted(envelope.updated_at || ""),
48
+ this.taskId(project, envelope.id),
49
+ ];
50
+ if (flags.verbose) {
51
+ row.push(this.muted(envelope.content_hash || ""));
52
+ }
53
+ rows.push(row);
54
+ }
55
+ this.table(rows);
56
+ }
57
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ import type { Project } from "../../types.js";
3
+ export default class TasksOpen extends BaseCommand {
4
+ static summary: string;
5
+ static description: string;
6
+ static args: {
7
+ target: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ taskIdForTarget(project: Project, target: string): Promise<string>;
10
+ openUrl(url: string): Promise<void>;
11
+ run(): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=open.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/open.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAIpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAc9C,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAChD,OAAgB,OAAO,SAAqC;IAC5D,OAAgB,WAAW,SAAuH;IAElJ,OAAgB,IAAI;;MAElB;IAEI,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBlE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAgB3B"}
@@ -0,0 +1,64 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Args } from "@oclif/core";
3
+ import { BaseCommand } from "../../base-command.js";
4
+ import { taskAdminUrl } from "../../config.js";
5
+ import { CliError } from "../../errors.js";
6
+ import { displayTaskPath, resolveTaskSelector } from "../../tasks.js";
7
+ function openerCommand(url) {
8
+ if (process.platform === "darwin") {
9
+ return { command: "open", args: [url] };
10
+ }
11
+ if (process.platform === "win32") {
12
+ return { command: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
13
+ }
14
+ return { command: "xdg-open", args: [url] };
15
+ }
16
+ export default class TasksOpen extends BaseCommand {
17
+ static summary = "Open a remote task in Mechanic.";
18
+ static description = "Open a remote Mechanic task by task ID, linked local task JSON file, helper directory, or unique local task slug.";
19
+ static args = {
20
+ target: Args.string({ required: true, description: "Remote task ID, linked local task JSON file, helper directory, or unique local task slug." }),
21
+ };
22
+ async taskIdForTarget(project, target) {
23
+ const selector = await resolveTaskSelector(project, target, {
24
+ allowHelperDir: true,
25
+ allowRemoteId: true,
26
+ });
27
+ if (selector.remoteId) {
28
+ return selector.remoteId;
29
+ }
30
+ if (selector.file || selector.helperDir) {
31
+ throw new CliError(`${displayTaskPath(project, selector.helperDir || selector.file || target)} is not linked to a remote task. Pull or publish it first.`, 2);
32
+ }
33
+ throw new CliError(`No remote task ID found for ${target}.`, 2);
34
+ }
35
+ async openUrl(url) {
36
+ const opener = openerCommand(url);
37
+ await new Promise((resolve, reject) => {
38
+ const child = spawn(opener.command, opener.args, {
39
+ detached: true,
40
+ stdio: "ignore",
41
+ });
42
+ child.once("error", reject);
43
+ child.once("spawn", () => {
44
+ child.unref();
45
+ resolve();
46
+ });
47
+ });
48
+ }
49
+ async run() {
50
+ const { args } = await this.parse(TasksOpen);
51
+ const project = await this.loadProject();
52
+ const taskId = await this.taskIdForTarget(project, args.target);
53
+ const url = taskAdminUrl(project, taskId);
54
+ try {
55
+ await this.openUrl(url);
56
+ this.log(`Opened ${this.taskId(project, taskId)}`);
57
+ }
58
+ catch {
59
+ this.warn("Could not open a browser automatically.");
60
+ this.log(`Open ${this.taskId(project, taskId)} manually:`);
61
+ }
62
+ this.log(this.muted(url));
63
+ }
64
+ }
@@ -0,0 +1,45 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ import type { MechanicClient } from "../../client.js";
3
+ import type { JsonObject, Project, TaskPreviewResponse } from "../../types.js";
4
+ type PreparedPreview = {
5
+ label: string;
6
+ slug?: string;
7
+ task?: JsonObject;
8
+ remoteId?: string;
9
+ };
10
+ type PreviewRenderOptions = {
11
+ verbose: boolean;
12
+ };
13
+ export default class TasksPreview extends BaseCommand {
14
+ static summary: string;
15
+ static description: string;
16
+ static args: {
17
+ target: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
18
+ };
19
+ static flags: {
20
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
21
+ remote: import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ };
24
+ run(): Promise<void>;
25
+ exitForStatus(response: TaskPreviewResponse): void;
26
+ preparePreview(project: Project, target: string, remote: boolean): Promise<PreparedPreview>;
27
+ requestPreview(client: MechanicClient, prepared: PreparedPreview, remote: boolean): Promise<TaskPreviewResponse>;
28
+ renderPreview(project: Project, prepared: PreparedPreview, response: TaskPreviewResponse, options: PreviewRenderOptions): void;
29
+ renderVerboseDetails(response: TaskPreviewResponse): void;
30
+ renderRunDetails(response: TaskPreviewResponse): void;
31
+ renderPreviewValue(label: string, value: unknown, indent: number): void;
32
+ formatPreviewValue(value: unknown): string | null;
33
+ runStatusLabel(ok: boolean | null): string;
34
+ renderPermissions(response: TaskPreviewResponse): void;
35
+ renderValidationErrors(response: TaskPreviewResponse): void;
36
+ previewTitle(prepared: PreparedPreview, response: TaskPreviewResponse): string;
37
+ sourceSummary(project: Project, prepared: PreparedPreview, response: TaskPreviewResponse): string;
38
+ uniqueScopes(scopes: string[]): string[];
39
+ validateLocalTask(task: JsonObject, label: string): void;
40
+ blockStaleHelperDir(project: Project, filePath: string, relativeFile: string, task: JsonObject): Promise<void>;
41
+ approvalHint(action: string): string;
42
+ statusLabel(status: TaskPreviewResponse["status"]): string;
43
+ }
44
+ export {};
45
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/preview.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAYpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE/E,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAWF,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,WAAW;IACnD,OAAgB,OAAO,SAAmD;IAC1E,OAAgB,WAAW,SAId;IAEb,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;;MAInB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB1B,aAAa,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAS5C,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IA0D3F,cAAc,CAClB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,mBAAmB,CAAC;IA4B/B,aAAa,CACX,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,oBAAoB,GAC5B,IAAI;IA+BP,oBAAoB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAuDzD,gBAAgB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IA+BrD,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAiBvE,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI;IAgBjD,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM;IAY1C,iBAAiB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IA4BtD,sBAAsB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAmB3D,YAAY,CAAC,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,mBAAmB,GAAG,MAAM;IAI9E,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,mBAAmB,GAAG,MAAM;IA+BjG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IAIxC,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAWlD,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBpH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAapC,WAAW,CAAC,MAAM,EAAE,mBAAmB,CAAC,QAAQ,CAAC,GAAG,MAAM;CAU3D"}