@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,40 @@
1
+ import type { JsonObject, ShopStatusResponse, TaskEnvelope, TaskListResponse, TaskPreviewResponse } from "./types.js";
2
+ export declare const USER_AGENT: string;
3
+ type RequestOptions = {
4
+ method?: string;
5
+ body?: unknown;
6
+ headers?: Record<string, string>;
7
+ retry?: boolean;
8
+ };
9
+ export type AuthVerification = {
10
+ shop: {
11
+ shopify_domain: string;
12
+ };
13
+ api_token?: {
14
+ name?: string | null;
15
+ created_by?: string | null;
16
+ };
17
+ };
18
+ export declare class MechanicClient {
19
+ readonly baseUrl: string;
20
+ readonly token: string;
21
+ constructor({ baseUrl, token }: {
22
+ baseUrl: string;
23
+ token: string;
24
+ });
25
+ request<T>(pathname: string, options?: RequestOptions): Promise<T>;
26
+ verifyAuth(): Promise<AuthVerification>;
27
+ getShopStatus(): Promise<ShopStatusResponse>;
28
+ listTasks(): Promise<TaskListResponse>;
29
+ getTask(id: string): Promise<TaskEnvelope>;
30
+ previewTask(task: JsonObject, id?: string): Promise<TaskPreviewResponse>;
31
+ previewRemoteTask(id: string): Promise<TaskPreviewResponse>;
32
+ createTask(task: JsonObject, idempotencyKey?: string): Promise<TaskEnvelope>;
33
+ updateTask(id: string, body: {
34
+ task: JsonObject;
35
+ previous_content_hash?: string;
36
+ force?: boolean;
37
+ }): Promise<TaskEnvelope>;
38
+ }
39
+ export {};
40
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAKtH,eAAO,MAAM,UAAU,QAAkD,CAAC;AAE1E,KAAK,cAAc,GAAG;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE;QACJ,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,SAAS,CAAC,EAAE;QACV,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,CAAC;CACH,CAAC;AAwGF,qBAAa,cAAc;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAK5D,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;IA+C5E,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAIvC,aAAa,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAI5C,SAAS,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAItC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI1C,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IASxE,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAM3D,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAQ5E,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC;CAM3H"}
package/dist/client.js ADDED
@@ -0,0 +1,172 @@
1
+ import { createRequire } from "node:module";
2
+ import { CliError, HttpError } from "./errors.js";
3
+ import { stableStringify } from "./json.js";
4
+ const require = createRequire(import.meta.url);
5
+ const packageJson = require("../package.json");
6
+ export const USER_AGENT = `MechanicCLI/${packageJson.version || "0.0.0"}`;
7
+ function sleep(milliseconds) {
8
+ return new Promise((resolve) => {
9
+ setTimeout(resolve, milliseconds);
10
+ });
11
+ }
12
+ function parseBody(text) {
13
+ if (!text) {
14
+ return null;
15
+ }
16
+ try {
17
+ return JSON.parse(text);
18
+ }
19
+ catch {
20
+ return { raw: text };
21
+ }
22
+ }
23
+ function retryDelayMilliseconds(response, attempt) {
24
+ const retryAfter = response.headers.get("retry-after");
25
+ if (retryAfter) {
26
+ const parsedSeconds = Number.parseFloat(retryAfter);
27
+ if (Number.isFinite(parsedSeconds) && parsedSeconds >= 0) {
28
+ return parsedSeconds * 1000;
29
+ }
30
+ const parsedDate = Date.parse(retryAfter);
31
+ if (Number.isFinite(parsedDate)) {
32
+ return Math.max(parsedDate - Date.now(), 0);
33
+ }
34
+ }
35
+ return 250 * (2 ** (attempt - 1));
36
+ }
37
+ function retryAfterMessage(response) {
38
+ const retryAfter = response.headers.get("retry-after");
39
+ if (!retryAfter) {
40
+ return null;
41
+ }
42
+ const seconds = Number.parseFloat(retryAfter);
43
+ if (Number.isFinite(seconds) && seconds >= 0) {
44
+ return `Try again in ${Math.ceil(seconds)} seconds.`;
45
+ }
46
+ const date = Date.parse(retryAfter);
47
+ if (Number.isFinite(date)) {
48
+ return `Try again after ${new Date(date).toISOString()}.`;
49
+ }
50
+ return null;
51
+ }
52
+ function networkErrorMessage(error) {
53
+ const cause = error && typeof error === "object" && "cause" in error
54
+ ? error.cause
55
+ : undefined;
56
+ if (cause instanceof Error && cause.message) {
57
+ return cause.message;
58
+ }
59
+ if (error instanceof Error && error.message) {
60
+ return error.message;
61
+ }
62
+ return String(error);
63
+ }
64
+ function apiErrorMessage(response, parsed) {
65
+ const body = parsed;
66
+ const serverMessage = body?.error?.message || body?.message;
67
+ switch (response.status) {
68
+ case 401:
69
+ return [
70
+ "Mechanic API rejected the API token.",
71
+ "Set MECHANIC_API_TOKEN or run \"mechanic auth login\" with a valid API token for this shop.",
72
+ ].join("\n");
73
+ case 403:
74
+ return [
75
+ "Mechanic API denied this request.",
76
+ "Check that the API token belongs to an installed, active Mechanic shop with access to this API.",
77
+ ].join("\n");
78
+ case 429:
79
+ return [
80
+ "Mechanic API rate limit exceeded.",
81
+ retryAfterMessage(response),
82
+ serverMessage,
83
+ ].filter(Boolean).join("\n");
84
+ default:
85
+ return serverMessage || `HTTP ${response.status}`;
86
+ }
87
+ }
88
+ export class MechanicClient {
89
+ baseUrl;
90
+ token;
91
+ constructor({ baseUrl, token }) {
92
+ this.baseUrl = baseUrl;
93
+ this.token = token;
94
+ }
95
+ async request(pathname, options = {}) {
96
+ const method = options.method || "GET";
97
+ const retry = options.retry ?? method === "GET";
98
+ const maxAttempts = retry ? 4 : 1;
99
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
100
+ const url = new URL(pathname, this.baseUrl);
101
+ let response;
102
+ try {
103
+ response = await fetch(url, {
104
+ method,
105
+ headers: {
106
+ Accept: "application/json",
107
+ Authorization: `Bearer ${this.token}`,
108
+ "User-Agent": USER_AGENT,
109
+ ...(options.body === undefined ? {} : { "Content-Type": "application/json" }),
110
+ ...(options.headers || {}),
111
+ },
112
+ body: options.body === undefined ? undefined : stableStringify(options.body),
113
+ });
114
+ }
115
+ catch (error) {
116
+ throw new CliError([
117
+ `Could not reach Mechanic API at ${url.origin}.`,
118
+ "Check that mechanic.json api_base_url points at the API you want to use.",
119
+ "For dev, re-run mechanic init with --api-base-url <your public API host> --force, or set MECHANIC_API_BASE_URL.",
120
+ `Network error: ${networkErrorMessage(error)}`,
121
+ ].join("\n"));
122
+ }
123
+ const parsed = parseBody(await response.text());
124
+ if (response.ok) {
125
+ return parsed;
126
+ }
127
+ if (retry && [429, 503].includes(response.status) && attempt < maxAttempts) {
128
+ await sleep(retryDelayMilliseconds(response, attempt));
129
+ continue;
130
+ }
131
+ throw new HttpError(apiErrorMessage(response, parsed), response.status, parsed);
132
+ }
133
+ throw new HttpError("Request failed", 0, null);
134
+ }
135
+ verifyAuth() {
136
+ return this.request("/v1/auth/verify");
137
+ }
138
+ getShopStatus() {
139
+ return this.request("/v1/shop/status");
140
+ }
141
+ listTasks() {
142
+ return this.request("/v1/tasks");
143
+ }
144
+ getTask(id) {
145
+ return this.request(`/v1/tasks/${encodeURIComponent(id)}`);
146
+ }
147
+ previewTask(task, id) {
148
+ const pathname = id ? `/v1/tasks/${encodeURIComponent(id)}/preview` : "/v1/tasks/preview";
149
+ return this.request(pathname, {
150
+ method: "POST",
151
+ body: { task },
152
+ });
153
+ }
154
+ previewRemoteTask(id) {
155
+ return this.request(`/v1/tasks/${encodeURIComponent(id)}/preview`, {
156
+ method: "POST",
157
+ });
158
+ }
159
+ createTask(task, idempotencyKey) {
160
+ return this.request("/v1/tasks", {
161
+ method: "POST",
162
+ headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : undefined,
163
+ body: { task },
164
+ });
165
+ }
166
+ updateTask(id, body) {
167
+ return this.request(`/v1/tasks/${encodeURIComponent(id)}`, {
168
+ method: "PUT",
169
+ body,
170
+ });
171
+ }
172
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class AuthLogin extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ };
7
+ run(): Promise<void>;
8
+ promptForToken(): Promise<string>;
9
+ }
10
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/login.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAIpD,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAChD,OAAgB,WAAW,SAA4C;IAEvE,OAAgB,KAAK;;MAInB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBpB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;CASxC"}
@@ -0,0 +1,36 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { promptForToken, saveToken } from "../../auth.js";
3
+ import { BaseCommand } from "../../base-command.js";
4
+ import { MechanicClient } from "../../client.js";
5
+ import { CliError } from "../../errors.js";
6
+ export default class AuthLogin extends BaseCommand {
7
+ static description = "Store and verify a Mechanic API token.";
8
+ static flags = {
9
+ token: Flags.string({
10
+ description: "Mechanic API token for controlled automation. Prefer MECHANIC_API_TOKEN or the masked prompt.",
11
+ }),
12
+ };
13
+ async run() {
14
+ const { flags } = await this.parse(AuthLogin);
15
+ const token = flags.token || process.env.MECHANIC_API_TOKEN || (await this.promptForToken());
16
+ const project = await this.loadProject();
17
+ const client = new MechanicClient({ baseUrl: project.apiBaseUrl, token });
18
+ const verification = await client.verifyAuth();
19
+ const verifiedDomain = verification.shop?.shopify_domain;
20
+ if (!verifiedDomain) {
21
+ throw new CliError("Token verification did not return a shop domain.");
22
+ }
23
+ if (verifiedDomain !== project.shopDomain) {
24
+ throw new CliError(`Token belongs to ${verifiedDomain}, but this project is configured for ${project.shopDomain}.`);
25
+ }
26
+ await saveToken(project.shopDomain, token);
27
+ this.log(`${this.success("Stored")} API token for ${this.accent(project.shopDomain)}`);
28
+ }
29
+ async promptForToken() {
30
+ const token = await promptForToken();
31
+ if (!token) {
32
+ throw new CliError("API token cannot be blank.");
33
+ }
34
+ return token;
35
+ }
36
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class AuthLogout extends BaseCommand {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=logout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/logout.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW;IACjD,OAAgB,WAAW,SAA4D;IAEjF,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAK3B"}
@@ -0,0 +1,10 @@
1
+ import { deleteToken } from "../../auth.js";
2
+ import { BaseCommand } from "../../base-command.js";
3
+ export default class AuthLogout extends BaseCommand {
4
+ static description = "Remove the stored Mechanic API token for this project.";
5
+ async run() {
6
+ const project = await this.loadProject();
7
+ await deleteToken(project.shopDomain);
8
+ this.log(`${this.color("yellow", "Removed")} API token for ${this.accent(project.shopDomain)}`);
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ import { BaseCommand } from "../base-command.js";
2
+ export default class Doctor extends BaseCommand {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ statusLabel(status: "ok" | "warn" | "fail"): string;
6
+ }
7
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAiBjD,MAAM,CAAC,OAAO,OAAO,MAAO,SAAQ,WAAW;IAC7C,OAAgB,WAAW,SAA2E;IAEhG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAkH1B,WAAW,CAAC,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM;CAUpD"}
@@ -0,0 +1,106 @@
1
+ import fs from "node:fs/promises";
2
+ import { requireToken } from "../auth.js";
3
+ import { BaseCommand } from "../base-command.js";
4
+ import { MechanicClient } from "../client.js";
5
+ import { CliError } from "../errors.js";
6
+ function errorMessage(error) {
7
+ return error instanceof Error ? error.message : String(error);
8
+ }
9
+ async function isDirectory(pathname) {
10
+ try {
11
+ return (await fs.stat(pathname)).isDirectory();
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ export default class Doctor extends BaseCommand {
18
+ static description = "Check local Mechanic CLI project configuration, auth, and API access.";
19
+ async run() {
20
+ const rows = [["Check", "Status", "Details"]];
21
+ let failed = false;
22
+ let project = null;
23
+ const addRow = (check, status, details = "") => {
24
+ if (status === "fail")
25
+ failed = true;
26
+ rows.push([
27
+ check,
28
+ this.statusLabel(status),
29
+ details,
30
+ ]);
31
+ };
32
+ try {
33
+ project = await this.loadProject();
34
+ addRow("Project", "ok", `${project.shopDomain} via ${project.apiBaseUrl}`);
35
+ }
36
+ catch (error) {
37
+ addRow("Project", "fail", errorMessage(error));
38
+ this.table(rows);
39
+ throw new CliError("Doctor found failed checks.", 2);
40
+ }
41
+ addRow("Tasks directory", await isDirectory(project.tasksDir) ? "ok" : "fail", project.tasksDirName);
42
+ const linkCount = Object.keys(project.links.tasks).length;
43
+ addRow("Task links", linkCount > 0 ? "ok" : "warn", linkCount === 1 ? "1 linked task" : `${linkCount} linked tasks`);
44
+ let token = null;
45
+ try {
46
+ token = await requireToken(project.shopDomain);
47
+ addRow("API token", "ok", process.env.MECHANIC_API_TOKEN?.trim() ? "MECHANIC_API_TOKEN" : "stored token");
48
+ }
49
+ catch (error) {
50
+ addRow("API token", "fail", errorMessage(error));
51
+ }
52
+ let verifiedShopMatches = false;
53
+ if (token) {
54
+ const client = new MechanicClient({
55
+ baseUrl: project.apiBaseUrl,
56
+ token,
57
+ });
58
+ try {
59
+ const verification = await client.verifyAuth();
60
+ const verifiedDomain = verification.shop?.shopify_domain;
61
+ verifiedShopMatches = verifiedDomain === project.shopDomain;
62
+ addRow("Token shop", verifiedShopMatches ? "ok" : "fail", verifiedDomain
63
+ ? `${verifiedDomain}${verifiedShopMatches ? "" : ` does not match ${project.shopDomain}`}`
64
+ : "API did not return a shop domain");
65
+ const tokenDetails = [
66
+ verification.api_token?.name,
67
+ verification.api_token?.created_by ? `created by ${verification.api_token.created_by}` : null,
68
+ ].filter(Boolean).join(", ");
69
+ if (tokenDetails) {
70
+ addRow("Token identity", "ok", tokenDetails);
71
+ }
72
+ }
73
+ catch (error) {
74
+ addRow("Token shop", "fail", errorMessage(error));
75
+ }
76
+ }
77
+ if (token && verifiedShopMatches) {
78
+ const client = new MechanicClient({
79
+ baseUrl: project.apiBaseUrl,
80
+ token,
81
+ });
82
+ try {
83
+ const response = await client.listTasks();
84
+ const taskCount = response.tasks.length;
85
+ addRow("Task API", "ok", taskCount === 1 ? "1 remote task" : `${taskCount} remote tasks`);
86
+ }
87
+ catch (error) {
88
+ addRow("Task API", "fail", errorMessage(error));
89
+ }
90
+ }
91
+ this.table(rows);
92
+ if (failed) {
93
+ throw new CliError("Doctor found failed checks.", 2);
94
+ }
95
+ }
96
+ statusLabel(status) {
97
+ switch (status) {
98
+ case "ok":
99
+ return this.success("ok");
100
+ case "warn":
101
+ return this.color("yellow", "warn");
102
+ case "fail":
103
+ return this.color("red", "fail");
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class GithubInit extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static flags: {
6
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/github/init.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAKpD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW;IACjD,OAAgB,OAAO,SAA4E;IACnG,OAAgB,WAAW,SAAiH;IAE5I,OAAgB,KAAK;;MAEnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA2C3B"}
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+ import { Flags } from "@oclif/core";
3
+ import { BaseCommand } from "../../base-command.js";
4
+ import { CliError } from "../../errors.js";
5
+ import { ensureDir, pathExists, writeText } from "../../fs.js";
6
+ import { githubWorkflowFiles } from "../../github-workflows.js";
7
+ export default class GithubInit extends BaseCommand {
8
+ static summary = "Create GitHub Actions workflows for single-shop Mechanic task updates.";
9
+ static description = "Create validation, manual deploy, and pull-from-app GitHub Actions workflows for this Mechanic CLI project.";
10
+ static flags = {
11
+ force: Flags.boolean({ description: "Overwrite existing Mechanic GitHub workflow files." }),
12
+ };
13
+ async run() {
14
+ const { flags } = await this.parse(GithubInit);
15
+ const project = await this.loadProject();
16
+ const workflowFiles = githubWorkflowFiles();
17
+ const existingFiles = [];
18
+ for (const workflowFile of workflowFiles) {
19
+ const filePath = path.join(project.cwd, workflowFile.path);
20
+ if (await pathExists(filePath)) {
21
+ existingFiles.push(workflowFile.path);
22
+ }
23
+ }
24
+ if (existingFiles.length > 0 && !flags.force) {
25
+ throw new CliError([
26
+ "Mechanic GitHub workflow files already exist:",
27
+ ...existingFiles.map((filePath) => `- ${filePath}`),
28
+ "",
29
+ "Re-run with --force to overwrite them.",
30
+ ].join("\n"), 2);
31
+ }
32
+ const rows = [["Workflow", "Path"]];
33
+ for (const workflowFile of workflowFiles) {
34
+ const filePath = path.join(project.cwd, workflowFile.path);
35
+ await ensureDir(path.dirname(filePath));
36
+ await writeText(filePath, workflowFile.contents);
37
+ rows.push([
38
+ this.success(flags.force && existingFiles.includes(workflowFile.path) ? "overwritten" : "created"),
39
+ this.taskName(workflowFile.path),
40
+ ]);
41
+ }
42
+ this.table(rows);
43
+ this.log("");
44
+ this.log("Next steps:");
45
+ this.log(` ${this.accent("gh secret set MECHANIC_API_TOKEN")}`);
46
+ this.log(" Paste a Mechanic API token for the shop in mechanic.json when prompted.");
47
+ this.log(" Commit mechanic.json, .mechanic/links.json, tasks/, .prettierignore, and .github/workflows/.");
48
+ this.log(" The pull-from-app workflow is manual by default; add a schedule later if you want automatic sync PRs.");
49
+ }
50
+ }
@@ -0,0 +1,7 @@
1
+ import { BaseCommand } from "../base-command.js";
2
+ export default class HelpCommand extends BaseCommand {
3
+ static description: string;
4
+ static strict: boolean;
5
+ run(): Promise<void>;
6
+ }
7
+ //# sourceMappingURL=help.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/commands/help.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,WAAW;IAClD,OAAgB,WAAW,SAAsC;IACjE,OAAgB,MAAM,UAAS;IAEzB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAI3B"}
@@ -0,0 +1,10 @@
1
+ import { Help } from "@oclif/core";
2
+ import { BaseCommand } from "../base-command.js";
3
+ export default class HelpCommand extends BaseCommand {
4
+ static description = "Show help for Mechanic commands.";
5
+ static strict = false;
6
+ async run() {
7
+ await new Help(this.config).showHelp(this.argv);
8
+ this.log(this.lightwardAiLine());
9
+ }
10
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from "../base-command.js";
2
+ export default class Init extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ shop: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
+ "api-base-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ "app-url": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AASjD,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,WAAW;IAC3C,OAAgB,WAAW,SAAkD;IAE7E,OAAgB,KAAK;;;;;;MAQnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA6D3B"}
@@ -0,0 +1,72 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { canPromptForToken, promptForToken, saveToken } from "../auth.js";
3
+ import { BaseCommand } from "../base-command.js";
4
+ import { MechanicClient } from "../client.js";
5
+ import { DEFAULT_API_BASE_URL, ensureMechanicPrettierIgnore, initProject, normalizeApiBaseUrl } from "../config.js";
6
+ import { CliError } from "../errors.js";
7
+ function errorMessage(error) {
8
+ return error instanceof Error ? error.message : String(error);
9
+ }
10
+ export default class Init extends BaseCommand {
11
+ static description = "Initialize a Mechanic CLI task sync project.";
12
+ static flags = {
13
+ shop: Flags.string({ required: true, description: "Shopify shop domain for this project." }),
14
+ "api-base-url": Flags.string({ description: "Mechanic API base URL.", default: DEFAULT_API_BASE_URL }),
15
+ "app-url": Flags.string({ description: "Mechanic embedded app root URL for clickable task links." }),
16
+ force: Flags.boolean({ description: "Overwrite existing mechanic.json while preserving task links." }),
17
+ token: Flags.string({
18
+ description: "Mechanic API token for controlled automation. Prefer MECHANIC_API_TOKEN or the masked prompt.",
19
+ }),
20
+ };
21
+ async run() {
22
+ const { flags } = await this.parse(Init);
23
+ const token = flags.token || process.env.MECHANIC_API_TOKEN || (canPromptForToken()
24
+ ? await promptForToken({
25
+ allowBlank: true,
26
+ prompt: "Mechanic API token (leave blank to add it later): ",
27
+ })
28
+ : null);
29
+ let storedToken = false;
30
+ if (token) {
31
+ const client = new MechanicClient({ baseUrl: normalizeApiBaseUrl(flags["api-base-url"]), token });
32
+ const verification = await client.verifyAuth();
33
+ const verifiedDomain = verification.shop?.shopify_domain;
34
+ if (!verifiedDomain) {
35
+ throw new CliError("Token verification did not return a shop domain.");
36
+ }
37
+ if (verifiedDomain !== flags.shop) {
38
+ throw new CliError(`Token belongs to ${verifiedDomain}, but this project is configured for ${flags.shop}.`);
39
+ }
40
+ }
41
+ const project = await initProject(process.cwd(), flags.shop, flags["api-base-url"], flags["app-url"], Boolean(flags.force));
42
+ try {
43
+ if (await ensureMechanicPrettierIgnore(process.cwd())) {
44
+ this.log(`${this.success("Updated")} .prettierignore for Mechanic Liquid files`);
45
+ }
46
+ }
47
+ catch (error) {
48
+ this.warn(`Could not update .prettierignore: ${errorMessage(error)}`);
49
+ }
50
+ if (token) {
51
+ await saveToken(project.shopDomain, token);
52
+ storedToken = true;
53
+ }
54
+ this.log(`${this.success("Initialized")} ${this.accent("Mechanic")} CLI project for ${this.accent(flags.shop)}`);
55
+ if (storedToken) {
56
+ this.log(`${this.success("Stored")} API token for ${this.accent(project.shopDomain)}`);
57
+ }
58
+ this.log("");
59
+ this.log("Next steps:");
60
+ if (!storedToken) {
61
+ this.log(` ${this.taskName("mechanic auth login")}`);
62
+ }
63
+ this.log(` ${this.taskName("mechanic tasks pull")}`);
64
+ this.log(` ${this.taskName("mechanic tasks status")}`);
65
+ this.log(` ${this.taskName("mechanic tasks preview tasks/<task>.json")}`);
66
+ this.log(` ${this.taskName("mechanic tasks diff tasks/<task>.json")}`);
67
+ this.log(` ${this.taskName("mechanic tasks publish tasks/<task>.json --dry-run")}`);
68
+ this.log(` ${this.taskName("mechanic tasks publish tasks/<task>.json")}`);
69
+ this.log("");
70
+ this.log(this.lightwardAiLine());
71
+ }
72
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class ShopStatus extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/commands/shop/status.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAkDpD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW;IACjD,OAAgB,OAAO,SAA4D;IACnF,OAAgB,WAAW,SAAyF;IAEpH,OAAgB,KAAK;;MAInB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAoG3B"}