@lightward/mechanic-cli 0.1.1 → 0.1.4

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 (49) hide show
  1. package/README.md +75 -37
  2. package/dist/base-command.d.ts +1 -0
  3. package/dist/base-command.d.ts.map +1 -1
  4. package/dist/base-command.js +16 -1
  5. package/dist/client.d.ts +3 -1
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +4 -1
  8. package/dist/commands/auth/login.d.ts.map +1 -1
  9. package/dist/commands/auth/login.js +6 -2
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +9 -7
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +12 -6
  14. package/dist/commands/tasks/bundle.d.ts.map +1 -1
  15. package/dist/commands/tasks/bundle.js +37 -5
  16. package/dist/commands/tasks/diff.d.ts.map +1 -1
  17. package/dist/commands/tasks/diff.js +2 -2
  18. package/dist/commands/tasks/new.d.ts +16 -0
  19. package/dist/commands/tasks/new.d.ts.map +1 -0
  20. package/dist/commands/tasks/new.js +104 -0
  21. package/dist/commands/tasks/open.d.ts.map +1 -1
  22. package/dist/commands/tasks/open.js +2 -2
  23. package/dist/commands/tasks/preview.d.ts +4 -0
  24. package/dist/commands/tasks/preview.d.ts.map +1 -1
  25. package/dist/commands/tasks/preview.js +43 -3
  26. package/dist/commands/tasks/publish.js +1 -1
  27. package/dist/commands/tasks/pull.d.ts.map +1 -1
  28. package/dist/commands/tasks/pull.js +8 -5
  29. package/dist/commands/tasks/push.d.ts +2 -0
  30. package/dist/commands/tasks/push.d.ts.map +1 -1
  31. package/dist/commands/tasks/push.js +22 -7
  32. package/dist/commands/tasks/status.d.ts +1 -1
  33. package/dist/commands/tasks/status.d.ts.map +1 -1
  34. package/dist/commands/tasks/status.js +26 -14
  35. package/dist/commands/tasks/unbundle.d.ts.map +1 -1
  36. package/dist/commands/tasks/unbundle.js +5 -5
  37. package/dist/commands/tasks/validate.d.ts +1 -0
  38. package/dist/commands/tasks/validate.d.ts.map +1 -1
  39. package/dist/commands/tasks/validate.js +40 -18
  40. package/dist/config.d.ts +1 -1
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/config.js +19 -11
  43. package/dist/tasks.js +2 -2
  44. package/dist/types.d.ts +12 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/update-check.d.ts +14 -0
  47. package/dist/update-check.d.ts.map +1 -0
  48. package/dist/update-check.js +136 -0
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -38,6 +38,8 @@ Install from npm:
38
38
  npm install -g @lightward/mechanic-cli
39
39
  ```
40
40
 
41
+ In interactive terminals, Mechanic occasionally checks npm and prints the update command when a newer CLI is available.
42
+
41
43
  ## Quick Start
42
44
 
43
45
  Create an API token in Mechanic:
@@ -60,20 +62,30 @@ Pull existing Mechanic tasks into local JSON files:
60
62
  mechanic tasks pull
61
63
  ```
62
64
 
65
+ To start from scratch instead, create a new blank local task. This writes a
66
+ starter JSON file and matching helper directory. You can use this in any
67
+ initialized CLI project; it does not require a fresh repository:
68
+
69
+ ```bash
70
+ mechanic tasks new order-tagger
71
+ ```
72
+
63
73
  For most Liquid or documentation edits, unbundle one task into helper files,
64
74
  edit those files, then bundle the helper directory back into its canonical JSON
65
75
  file. The JSON file remains the deployable source of truth; the helper directory
66
- is the comfortable editing view beside it.
76
+ is the comfortable editing view beside it. Tasks created with `tasks new`
77
+ already have both files, so you can start editing the helper directory
78
+ immediately.
67
79
 
68
80
  ```bash
69
- mechanic tasks unbundle tasks/order-tagger.json
81
+ mechanic tasks unbundle order-tagger
70
82
  # edit tasks/order-tagger/script.liquid, docs.md, or task.json
71
- mechanic tasks bundle tasks/order-tagger.json
83
+ mechanic tasks bundle order-tagger
72
84
  mechanic tasks status
73
- mechanic tasks preview tasks/order-tagger.json
74
- mechanic tasks diff tasks/order-tagger.json
75
- mechanic tasks publish tasks/order-tagger.json --dry-run
76
- mechanic tasks publish tasks/order-tagger.json
85
+ mechanic tasks preview order-tagger
86
+ mechanic tasks diff order-tagger
87
+ mechanic tasks publish order-tagger --dry-run
88
+ mechanic tasks publish order-tagger
77
89
  ```
78
90
 
79
91
  For normal setup, paste the token into the masked prompt or run
@@ -102,8 +114,9 @@ mechanic auth logout
102
114
  mechanic github init [--force]
103
115
  mechanic shop status [--json]
104
116
  mechanic tasks list [--verbose] [--json]
117
+ mechanic tasks new <name> [--force] [--json]
105
118
  mechanic tasks open <task>
106
- mechanic tasks status [task] [--remote] [--json]
119
+ mechanic tasks status [task] [--local] [--json]
107
120
  mechanic tasks pull [--force]
108
121
  mechanic tasks pull <task> [--force]
109
122
  mechanic tasks pull --all [--force]
@@ -113,38 +126,61 @@ mechanic tasks diff --all [--exit-code] [--json]
113
126
  mechanic tasks publish <task> [--force] [--dry-run] [--json]
114
127
  mechanic tasks publish --all [--force] [--dry-run] [--json]
115
128
  mechanic tasks unbundle <task> [--out <dir>] [--json]
116
- mechanic tasks bundle <dir|file> [--out <file>] [--json]
117
- mechanic tasks validate <file|dir> [--json]
129
+ mechanic tasks bundle <task|dir|file> [--out <file>] [--json]
130
+ mechanic tasks validate <task|dir> [--json]
118
131
  ```
119
132
 
120
- Most task commands accept a `<task>` selector. A task selector can be a local
121
- JSON file, a helper directory, a linked remote task ID, or a unique local task
122
- slug:
133
+ Most task commands accept a `<task>` selector. In day-to-day use, prefer the
134
+ short local task slug:
135
+
136
+ ```bash
137
+ mechanic tasks preview order-tagger
138
+ ```
139
+
140
+ The CLI also accepts a full task JSON path, matching helper directory, or linked
141
+ remote task ID:
123
142
 
124
143
  ```bash
125
144
  mechanic tasks preview tasks/order-tagger.json
126
145
  mechanic tasks preview tasks/order-tagger
127
- mechanic tasks preview order-tagger
128
146
  mechanic tasks preview 171578bf-79e2-46af-857a-dbd71c6b7b2b
129
147
  ```
130
148
 
131
149
  The local file is the working copy. The remote task ID is the Mechanic app's
132
150
  address for that task. `tasks list` shows linked local files when the CLI knows
133
- them. If a short selector matches more than one local file, the CLI asks for the
134
- full file path.
151
+ them. If a short slug matches more than one local file, the CLI asks for the
152
+ full file path; that is the main reason to use `tasks/<name>.json` directly.
153
+
154
+ Task names and local file names are related, but they are not the same identity.
155
+ When the CLI first pulls or creates a task, it uses the task name to choose a
156
+ readable local slug like `order-tagger`, which becomes `tasks/order-tagger.json`.
157
+ After that, the remote task ID stored in `.mechanic/links.json` is the sync
158
+ identity. If you rename the task in Mechanic, the next pull keeps the existing
159
+ local file name and updates the `name` field inside the JSON. If you rename the
160
+ local JSON file or helper directory by hand, the CLI treats that as a new local
161
+ slug and the task may appear unlinked. Keep local filenames stable unless you
162
+ also intentionally update `.mechanic/links.json` and verify with `tasks status`
163
+ and `tasks publish --dry-run`.
135
164
 
136
165
  `tasks pull` pulls every task when you run it without arguments. Pass a task
137
166
  selector when you only want one task. `tasks diff` and `tasks publish` operate
138
167
  on one task when you pass a selector. They operate on every task only when you
139
168
  explicitly pass `--all`.
140
169
 
170
+ `tasks new` creates a new blank local starter task. It writes both
171
+ `tasks/<slug>.json` and `tasks/<slug>/`, so you can edit the helper files first
172
+ and bundle them into the JSON file before publishing. It can be used any time in
173
+ an initialized CLI project, and it refuses to overwrite existing local task files
174
+ unless you pass `--force`. It does not create anything in Mechanic until you run
175
+ `tasks publish`; new published tasks are created disabled.
176
+
141
177
  `shop status` shows the current Mechanic run queue for the configured shop:
142
178
  running runs, waiting runs, queue lag, and the largest backlog groups by task,
143
179
  action, and event topic.
144
180
 
145
- `tasks status` shows whether local task JSON files are linked to remote Mechanic
146
- tasks and ready to publish. It is local-only by default; add `--remote` to check
147
- whether linked remote tasks are unchanged, locally changed, or conflicted.
181
+ `tasks status` shows whether local task JSON files are linked, ready to publish,
182
+ and in sync with their remote Mechanic tasks. Use `--local` when you only want
183
+ the fast offline check for JSON validity, link state, and helper-folder drift.
148
184
  Add `--json` when an editor integration needs task readiness, link state, and
149
185
  remote sync state without parsing table output.
150
186
 
@@ -165,21 +201,22 @@ or enabling the task.
165
201
 
166
202
  `tasks publish` sends local task JSON back to Mechanic.
167
203
 
168
- `tasks publish --dry-run` validates the selected files, checks for unbundled
169
- helper changes, fetches linked remote tasks, checks unlinked files against
170
- existing remote task names, and prints what would create, update, stay
171
- unchanged, or conflict. It does not write to Mechanic, `.mechanic/links.json`,
172
- or local task JSON. New tasks created by `tasks publish` are created disabled;
173
- review and enable them in Mechanic when they are ready to run.
204
+ `tasks publish --dry-run` is a publish preflight. It checks whether publishing
205
+ would be safe, including helper-folder drift, linked remote task changes, and
206
+ unlinked files that look like existing remote tasks. It then prints what would
207
+ create, update, stay unchanged, or stop as a conflict. It does not write to
208
+ Mechanic, `.mechanic/links.json`, or local task JSON. New tasks created by
209
+ `tasks publish` are created disabled; review and enable them in Mechanic when
210
+ they are ready to run.
174
211
 
175
212
  For automation or editor integrations, add `--json` to `tasks list`, `tasks
176
213
  status`, `tasks diff`, `tasks validate`, `tasks bundle`, `tasks unbundle`,
177
214
  `tasks publish`, or `shop status`.
178
215
 
179
216
  `tasks unbundle` and `tasks bundle` operate on one task at a time. By default,
180
- `mechanic tasks unbundle tasks/order-tagger.json` writes to
181
- `tasks/order-tagger/`, and `mechanic tasks bundle tasks/order-tagger.json`
182
- writes back to `tasks/order-tagger.json`.
217
+ `mechanic tasks unbundle order-tagger` writes to `tasks/order-tagger/`, and
218
+ `mechanic tasks bundle order-tagger` writes back to `tasks/order-tagger.json`.
219
+ You can pass the full JSON path when you want to be explicit.
183
220
 
184
221
  When a task has `subscriptions_template`, the editable helper file is
185
222
  `subscriptions.liquid`. Mechanic renders that template into `subscriptions`, so
@@ -207,8 +244,7 @@ open a task explicitly. Use `--app-url` or `MECHANIC_APP_URL` if your shop uses
207
244
  a non-production Shopify app alias.
208
245
 
209
246
  Use `mechanic tasks open <task>` to open a task in the Mechanic app from its
210
- remote ID, linked local task JSON file, helper directory, or unique local task
211
- slug.
247
+ local task slug, linked local task JSON file, helper directory, or remote ID.
212
248
 
213
249
  ## Advanced: GitHub Actions Task Workflows
214
250
 
@@ -322,7 +358,7 @@ inside JSON is unpleasant. The helper workflow splits a task into separate
322
358
  editable files:
323
359
 
324
360
  ```bash
325
- mechanic tasks unbundle tasks/order-tagger.json --out order-tagger
361
+ mechanic tasks unbundle order-tagger --out order-tagger
326
362
  ```
327
363
 
328
364
  `tasks unbundle` also accepts the same local task selectors as preview, diff,
@@ -330,6 +366,7 @@ and publish:
330
366
 
331
367
  ```bash
332
368
  mechanic tasks unbundle order-tagger
369
+ mechanic tasks unbundle tasks/order-tagger.json
333
370
  mechanic tasks unbundle tasks/order-tagger
334
371
  mechanic tasks unbundle 171578bf-79e2-46af-857a-dbd71c6b7b2b
335
372
  ```
@@ -347,14 +384,15 @@ order-tagger/
347
384
  After editing, bundle the helper directory back to canonical JSON:
348
385
 
349
386
  ```bash
350
- mechanic tasks bundle tasks/order-tagger.json
351
- mechanic tasks validate tasks/order-tagger.json
387
+ mechanic tasks bundle order-tagger
388
+ mechanic tasks validate order-tagger
352
389
  ```
353
390
 
354
- `tasks bundle` accepts either side of the pair. `mechanic tasks bundle
355
- tasks/order-tagger` writes `tasks/order-tagger.json`, and `mechanic tasks bundle
356
- tasks/order-tagger.json` reads `tasks/order-tagger/` and writes back to that JSON
357
- file.
391
+ `tasks bundle` accepts the local task slug or either side of the pair.
392
+ `mechanic tasks bundle order-tagger` and `mechanic tasks bundle
393
+ tasks/order-tagger` both write `tasks/order-tagger.json`. `mechanic tasks bundle
394
+ tasks/order-tagger.json` reads `tasks/order-tagger/` and writes back to that
395
+ JSON file.
358
396
 
359
397
  When unbundling, helper files are written only for string fields. `null` fields
360
398
  stay in `task.json`, and stale helper files for non-string fields are removed.
@@ -2,6 +2,7 @@ import { Command } from "@oclif/core";
2
2
  import { MechanicClient } from "./client.js";
3
3
  import type { Project } from "./types.js";
4
4
  export declare abstract class BaseCommand extends Command {
5
+ protected finally(error: Error | undefined): Promise<void>;
5
6
  loadProject(): Promise<Project>;
6
7
  clientForProject(project: Project): Promise<MechanicClient>;
7
8
  verifiedClientForProject(project: Project): Promise<MechanicClient>;
@@ -1 +1 @@
1
- {"version":3,"file":"base-command.d.ts","sourceRoot":"","sources":["../src/base-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAM,MAAM,aAAa,CAAC;AAI1C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,8BAAsB,WAAY,SAAQ,OAAO;IACzC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/B,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAO3D,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAMnE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI5B,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAenC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAI1C,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI3B,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAIhC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI7B,eAAe,IAAI,MAAM;IAQzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI9B,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAIhD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI;CAgB9B"}
1
+ {"version":3,"file":"base-command.d.ts","sourceRoot":"","sources":["../src/base-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAM,MAAM,aAAa,CAAC;AAI1C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAI7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,8BAAsB,WAAY,SAAQ,OAAO;cACtB,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnE,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/B,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAQ3D,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAMnE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI5B,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAenC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAI1C,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI3B,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAIhC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI7B,eAAe,IAAI,MAAM;IAQzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI9B,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAIhD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI;CAgB9B"}
@@ -5,13 +5,28 @@ import { requireToken } from "./auth.js";
5
5
  import { MechanicClient } from "./client.js";
6
6
  import { loadProject, taskAdminUrl } from "./config.js";
7
7
  import { CliError } from "./errors.js";
8
+ import { getUpdateNotice, shouldCheckForUpdates } from "./update-check.js";
8
9
  export class BaseCommand extends Command {
10
+ async finally(error) {
11
+ await super.finally(error);
12
+ if (error || !shouldCheckForUpdates(this.argv)) {
13
+ return;
14
+ }
15
+ const notice = await getUpdateNotice();
16
+ if (!notice) {
17
+ return;
18
+ }
19
+ this.log("");
20
+ this.log(`${this.color("yellow", "Update available")} Mechanic CLI ${this.muted(notice.currentVersion)} -> ${this.success(notice.latestVersion)}`);
21
+ this.log(`Update with: ${this.taskName("npm install -g @lightward/mechanic-cli")}`);
22
+ }
9
23
  async loadProject() {
10
24
  return loadProject(process.cwd());
11
25
  }
12
26
  async clientForProject(project) {
13
27
  return new MechanicClient({
14
28
  baseUrl: project.apiBaseUrl,
29
+ expectedShopDomain: project.shopDomain,
15
30
  token: await requireToken(project.shopDomain),
16
31
  });
17
32
  }
@@ -24,7 +39,7 @@ export class BaseCommand extends Command {
24
39
  const verification = await client.verifyAuth();
25
40
  const verifiedShopDomain = verification.shop?.shopify_domain;
26
41
  if (verifiedShopDomain !== project.shopDomain) {
27
- throw new CliError(`API token belongs to ${verifiedShopDomain || "an unknown shop"}, but mechanic.json is configured for ${project.shopDomain}.`, 2);
42
+ throw new CliError("API token does not match this Mechanic CLI project. Use a token for the shop in mechanic.json, or re-run mechanic init for the intended shop.", 2);
28
43
  }
29
44
  }
30
45
  accent(text) {
package/dist/client.d.ts CHANGED
@@ -17,9 +17,11 @@ export type AuthVerification = {
17
17
  };
18
18
  export declare class MechanicClient {
19
19
  readonly baseUrl: string;
20
+ readonly expectedShopDomain?: string;
20
21
  readonly token: string;
21
- constructor({ baseUrl, token }: {
22
+ constructor({ baseUrl, expectedShopDomain, token }: {
22
23
  baseUrl: string;
24
+ expectedShopDomain?: string;
23
25
  token: string;
24
26
  });
25
27
  request<T>(pathname: string, options?: RequestOptions): Promise<T>;
@@ -1 +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"}
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,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,EAAE,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAM7G,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;IAgD5E,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 CHANGED
@@ -87,9 +87,11 @@ function apiErrorMessage(response, parsed) {
87
87
  }
88
88
  export class MechanicClient {
89
89
  baseUrl;
90
+ expectedShopDomain;
90
91
  token;
91
- constructor({ baseUrl, token }) {
92
+ constructor({ baseUrl, expectedShopDomain, token }) {
92
93
  this.baseUrl = baseUrl;
94
+ this.expectedShopDomain = expectedShopDomain;
93
95
  this.token = token;
94
96
  }
95
97
  async request(pathname, options = {}) {
@@ -106,6 +108,7 @@ export class MechanicClient {
106
108
  Accept: "application/json",
107
109
  Authorization: `Bearer ${this.token}`,
108
110
  "User-Agent": USER_AGENT,
111
+ ...(this.expectedShopDomain ? { "X-Mechanic-Shop-Domain": this.expectedShopDomain } : {}),
109
112
  ...(options.body === undefined ? {} : { "Content-Type": "application/json" }),
110
113
  ...(options.headers || {}),
111
114
  },
@@ -1 +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"}
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;IA4BpB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;CASxC"}
@@ -14,14 +14,18 @@ export default class AuthLogin extends BaseCommand {
14
14
  const { flags } = await this.parse(AuthLogin);
15
15
  const token = flags.token || process.env.MECHANIC_API_TOKEN || (await this.promptForToken());
16
16
  const project = await this.loadProject();
17
- const client = new MechanicClient({ baseUrl: project.apiBaseUrl, token });
17
+ const client = new MechanicClient({
18
+ baseUrl: project.apiBaseUrl,
19
+ expectedShopDomain: project.shopDomain,
20
+ token,
21
+ });
18
22
  const verification = await client.verifyAuth();
19
23
  const verifiedDomain = verification.shop?.shopify_domain;
20
24
  if (!verifiedDomain) {
21
25
  throw new CliError("Token verification did not return a shop domain.");
22
26
  }
23
27
  if (verifiedDomain !== project.shopDomain) {
24
- throw new CliError(`Token belongs to ${verifiedDomain}, but this project is configured for ${project.shopDomain}.`);
28
+ throw new CliError("API token does not match this Mechanic CLI project. Use a token for the shop in mechanic.json.", 2);
25
29
  }
26
30
  await saveToken(project.shopDomain, token);
27
31
  this.log(`${this.success("Stored")} API token for ${this.accent(project.shopDomain)}`);
@@ -1 +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"}
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;IAoH1B,WAAW,CAAC,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM;CAUpD"}
@@ -60,14 +60,16 @@ export default class Doctor extends BaseCommand {
60
60
  const verifiedDomain = verification.shop?.shopify_domain;
61
61
  verifiedShopMatches = verifiedDomain === project.shopDomain;
62
62
  addRow("Token shop", verifiedShopMatches ? "ok" : "fail", verifiedDomain
63
- ? `${verifiedDomain}${verifiedShopMatches ? "" : ` does not match ${project.shopDomain}`}`
63
+ ? (verifiedShopMatches ? project.shopDomain : "API token does not match this project")
64
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);
65
+ if (verifiedShopMatches) {
66
+ const tokenDetails = [
67
+ verification.api_token?.name,
68
+ verification.api_token?.created_by ? `created by ${verification.api_token.created_by}` : null,
69
+ ].filter(Boolean).join(", ");
70
+ if (tokenDetails) {
71
+ addRow("Token identity", "ok", tokenDetails);
72
+ }
71
73
  }
72
74
  }
73
75
  catch (error) {
@@ -1 +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"}
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;CAoE3B"}
@@ -28,14 +28,18 @@ export default class Init extends BaseCommand {
28
28
  : null);
29
29
  let storedToken = false;
30
30
  if (token) {
31
- const client = new MechanicClient({ baseUrl: normalizeApiBaseUrl(flags["api-base-url"]), token });
31
+ const client = new MechanicClient({
32
+ baseUrl: normalizeApiBaseUrl(flags["api-base-url"]),
33
+ expectedShopDomain: flags.shop,
34
+ token,
35
+ });
32
36
  const verification = await client.verifyAuth();
33
37
  const verifiedDomain = verification.shop?.shopify_domain;
34
38
  if (!verifiedDomain) {
35
39
  throw new CliError("Token verification did not return a shop domain.");
36
40
  }
37
41
  if (verifiedDomain !== flags.shop) {
38
- throw new CliError(`Token belongs to ${verifiedDomain}, but this project is configured for ${flags.shop}.`);
42
+ throw new CliError("API token does not match the requested shop. Check --shop or use a token for that shop.", 2);
39
43
  }
40
44
  }
41
45
  const project = await initProject(process.cwd(), flags.shop, flags["api-base-url"], flags["app-url"], Boolean(flags.force));
@@ -61,11 +65,13 @@ export default class Init extends BaseCommand {
61
65
  this.log(` ${this.taskName("mechanic auth login")}`);
62
66
  }
63
67
  this.log(` ${this.taskName("mechanic tasks pull")}`);
68
+ this.log(" or, to start from scratch:");
69
+ this.log(` ${this.taskName("mechanic tasks new <task-slug>")}`);
64
70
  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")}`);
71
+ this.log(` ${this.taskName("mechanic tasks preview <task-slug>")}`);
72
+ this.log(` ${this.taskName("mechanic tasks diff <task-slug>")}`);
73
+ this.log(` ${this.taskName("mechanic tasks publish <task-slug> --dry-run")}`);
74
+ this.log(` ${this.taskName("mechanic tasks publish <task-slug>")}`);
69
75
  this.log("");
70
76
  this.log(this.lightwardAiLine());
71
77
  }
@@ -1 +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"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/bundle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AASpD,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,WAAW;IAClD,OAAgB,OAAO,SAA+D;IACtF,OAAgB,WAAW,SAAqG;IAChI,OAAgB,QAAQ,WAKtB;IAEF,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;MAOnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YAgCZ,kBAAkB;CAuEjC"}
@@ -3,17 +3,21 @@ import path from "node:path";
3
3
  import { BaseCommand } from "../../base-command.js";
4
4
  import { CliError } from "../../errors.js";
5
5
  import { pathExists } from "../../fs.js";
6
- import { bundleTask } from "../../tasks.js";
6
+ import { bundleTask, displayTaskPath, resolveTaskSelector } from "../../tasks.js";
7
+ function looksLikeTaskPath(input) {
8
+ return input.includes("/") || input.includes("\\") || input.toLowerCase().endsWith(".json");
9
+ }
7
10
  export default class TasksBundle extends BaseCommand {
8
11
  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.";
12
+ static description = "Bundle one local task slug or editable helper task directory into one canonical task JSON file.";
10
13
  static examples = [
14
+ "$ mechanic tasks bundle order-tagger",
11
15
  "$ mechanic tasks bundle tasks/order-tagger",
12
16
  "$ mechanic tasks bundle tasks/order-tagger.json",
13
- "$ mechanic tasks bundle tasks/order-tagger --out tasks/order-tagger.json",
17
+ "$ mechanic tasks bundle order-tagger --out tasks/order-tagger.json",
14
18
  ];
15
19
  static args = {
16
- dir: Args.string({ required: false, description: "Helper task directory, or its matching task JSON file." }),
20
+ dir: Args.string({ required: false, description: "Local task slug, helper task directory, or matching task JSON file." }),
17
21
  };
18
22
  static flags = {
19
23
  json: Flags.boolean({
@@ -30,7 +34,7 @@ export default class TasksBundle extends BaseCommand {
30
34
  "Choose one helper task directory to bundle.",
31
35
  "",
32
36
  "Example:",
33
- " mechanic tasks bundle tasks/order-tagger",
37
+ " mechanic tasks bundle order-tagger",
34
38
  "",
35
39
  "By default this writes to tasks/order-tagger.json. Use --out <file> to choose another task JSON file.",
36
40
  ].join("\n"), 2);
@@ -59,6 +63,34 @@ export default class TasksBundle extends BaseCommand {
59
63
  outputDisplay: out || `${input}.json`,
60
64
  };
61
65
  }
66
+ if (!looksLikeTaskPath(input)) {
67
+ let project = null;
68
+ try {
69
+ project = await this.loadProject();
70
+ }
71
+ catch (error) {
72
+ if (!(error instanceof CliError) || !error.message.startsWith("No mechanic.json found")) {
73
+ throw error;
74
+ }
75
+ }
76
+ if (project) {
77
+ const selector = await resolveTaskSelector(project, input, { allowHelperDir: true });
78
+ const filePath = selector.file ? path.resolve(selector.file) : null;
79
+ const helperPath = selector.helperDir
80
+ ? path.resolve(selector.helperDir)
81
+ : filePath?.replace(/\.json$/i, "");
82
+ if (helperPath && await pathExists(path.join(helperPath, "task.json"))) {
83
+ const outputPath = out
84
+ ? path.resolve(out)
85
+ : filePath || `${helperPath}.json`;
86
+ return {
87
+ dirPath: helperPath,
88
+ inputDisplay: displayTaskPath(project, helperPath),
89
+ outputDisplay: displayTaskPath(project, outputPath),
90
+ };
91
+ }
92
+ }
93
+ }
62
94
  if (!input.toLowerCase().endsWith(".json")) {
63
95
  return {
64
96
  dirPath: inputPath,
@@ -1 +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"}
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;CAoH3B"}
@@ -6,7 +6,7 @@ export default class TasksDiff extends BaseCommand {
6
6
  static summary = "Diff one task, or use --all to diff every local task.";
7
7
  static description = "Compare one local task with its linked remote task, or use --all to compare every local task JSON file.";
8
8
  static args = {
9
- file: Args.string({ required: false, description: "Task file, helper directory, linked task ID, or unique local task slug." }),
9
+ file: Args.string({ required: false, description: "Local task slug, task file, helper directory, or linked task ID." }),
10
10
  };
11
11
  static flags = {
12
12
  all: Flags.boolean({ description: "Diff every task JSON file in this project." }),
@@ -86,7 +86,7 @@ export default class TasksDiff extends BaseCommand {
86
86
  if (remoteChanged || diff) {
87
87
  const section = [`# ${relativeFile}`];
88
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.`), "");
89
+ section.push(this.color("yellow", "Remote changed since your last pull."), this.muted("Showing what would change if your local file replaced the current Mechanic task."), this.muted("Diff legend: - current Mechanic, + local file."), this.muted(`Run "mechanic tasks pull ${link.remote_id}" to keep the Mechanic version, or use --force only if the local file should win.`), "");
90
90
  }
91
91
  if (diff) {
92
92
  section.push(diff);
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class TasksNew extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ private checkExistingPaths;
15
+ }
16
+ //# sourceMappingURL=new.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/new.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAwCpD,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,WAAW;IAC/C,OAAgB,OAAO,SAAoC;IAC3D,OAAgB,WAAW,SAAiJ;IAC5K,OAAgB,QAAQ,WAGtB;IAEF,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;MAGnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YA0CZ,kBAAkB;CA2BjC"}