@lightward/mechanic-cli 0.1.4 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,10 +184,11 @@ the fast offline check for JSON validity, link state, and helper-folder drift.
184
184
  Add `--json` when an editor integration needs task readiness, link state, and
185
185
  remote sync state without parsing table output.
186
186
 
187
- `tasks diff` compares the current remote task to your local file, groups output
188
- by changed field, and only shows nearby changed lines. Differences are
189
- informational by default; add `--exit-code` when a script or CI job should treat
190
- differences as a nonzero result.
187
+ `tasks diff` compares current Mechanic with your local file, groups output by
188
+ changed field, and only shows nearby changed lines. In diff output, `-` means
189
+ current Mechanic and `+` means local file. Differences are informational by
190
+ default; add `--exit-code` when a script or CI job should treat differences as a
191
+ nonzero result.
191
192
 
192
193
  `tasks preview` is the confidence check before publishing. It sends one local
193
194
  task to Mechanic's preview engine without saving it, then reports whether the
@@ -412,12 +413,15 @@ Mechanic app or by another local checkout.
412
413
 
413
414
  - `tasks pull` records the latest remote content hash in `.mechanic/links.json`.
414
415
  - `tasks pull <task>` can refresh a linked local file without `--force` when the
415
- local file still matches the last pulled hash. If local edits would be lost,
416
+ local file still matches the last synced hash. If local edits would be lost,
416
417
  it stops and asks for an explicit `--force`.
417
418
  - `tasks publish` sends that hash as `previous_content_hash`.
418
419
  - The API rejects stale publishes with a conflict.
419
- - `tasks diff` warns when the remote task changed since your last pull, then
420
- compares the current Mechanic task with your local file.
420
+ - `tasks diff` warns when Mechanic changed since the file was last synced, then
421
+ compares current Mechanic with your local file. If only Mechanic changed, pull
422
+ normally to update your local file. If both Mechanic and your local file
423
+ changed, choose explicitly: pull with `--force` to replace local with
424
+ Mechanic, or publish with `--force` to overwrite Mechanic with local.
421
425
  - `tasks publish --all` checks every linked remote task before writing anything,
422
426
  so one stale task blocks the whole publish instead of partially publishing
423
427
  earlier files first.
@@ -38,7 +38,7 @@ export class BaseCommand extends Command {
38
38
  async verifyClientShop(project, client) {
39
39
  const verification = await client.verifyAuth();
40
40
  const verifiedShopDomain = verification.shop?.shopify_domain;
41
- if (verifiedShopDomain !== project.shopDomain) {
41
+ if (verifiedShopDomain && verifiedShopDomain !== project.shopDomain) {
42
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);
43
43
  }
44
44
  }
package/dist/client.d.ts CHANGED
@@ -7,9 +7,9 @@ type RequestOptions = {
7
7
  retry?: boolean;
8
8
  };
9
9
  export type AuthVerification = {
10
- shop: {
11
- shopify_domain: string;
12
- };
10
+ shop?: {
11
+ shopify_domain?: string | null;
12
+ } | null;
13
13
  api_token?: {
14
14
  name?: string | null;
15
15
  created_by?: string | null;
@@ -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,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"}
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,CAAC,EAAE;QACL,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAChC,GAAG,IAAI,CAAC;IACT,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"}
@@ -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;IA4BpB,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;IAwBpB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;CASxC"}
@@ -21,10 +21,7 @@ export default class AuthLogin extends BaseCommand {
21
21
  });
22
22
  const verification = await client.verifyAuth();
23
23
  const verifiedDomain = verification.shop?.shopify_domain;
24
- if (!verifiedDomain) {
25
- throw new CliError("Token verification did not return a shop domain.");
26
- }
27
- if (verifiedDomain !== project.shopDomain) {
24
+ if (verifiedDomain && verifiedDomain !== project.shopDomain) {
28
25
  throw new CliError("API token does not match this Mechanic CLI project. Use a token for the shop in mechanic.json.", 2);
29
26
  }
30
27
  await saveToken(project.shopDomain, token);
@@ -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;IAoH1B,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;IAsH1B,WAAW,CAAC,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM;CAUpD"}
@@ -53,15 +53,16 @@ export default class Doctor extends BaseCommand {
53
53
  if (token) {
54
54
  const client = new MechanicClient({
55
55
  baseUrl: project.apiBaseUrl,
56
+ expectedShopDomain: project.shopDomain,
56
57
  token,
57
58
  });
58
59
  try {
59
60
  const verification = await client.verifyAuth();
60
61
  const verifiedDomain = verification.shop?.shopify_domain;
61
- verifiedShopMatches = verifiedDomain === project.shopDomain;
62
+ verifiedShopMatches = !verifiedDomain || verifiedDomain === project.shopDomain;
62
63
  addRow("Token shop", verifiedShopMatches ? "ok" : "fail", verifiedDomain
63
64
  ? (verifiedShopMatches ? project.shopDomain : "API token does not match this project")
64
- : "API did not return a shop domain");
65
+ : `verified for ${project.shopDomain}`);
65
66
  if (verifiedShopMatches) {
66
67
  const tokenDetails = [
67
68
  verification.api_token?.name,
@@ -79,6 +80,7 @@ export default class Doctor extends BaseCommand {
79
80
  if (token && verifiedShopMatches) {
80
81
  const client = new MechanicClient({
81
82
  baseUrl: project.apiBaseUrl,
83
+ expectedShopDomain: project.shopDomain,
82
84
  token,
83
85
  });
84
86
  try {
@@ -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;CAoE3B"}
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;CAgE3B"}
@@ -35,10 +35,7 @@ export default class Init extends BaseCommand {
35
35
  });
36
36
  const verification = await client.verifyAuth();
37
37
  const verifiedDomain = verification.shop?.shopify_domain;
38
- if (!verifiedDomain) {
39
- throw new CliError("Token verification did not return a shop domain.");
40
- }
41
- if (verifiedDomain !== flags.shop) {
38
+ if (verifiedDomain && verifiedDomain !== flags.shop) {
42
39
  throw new CliError("API token does not match the requested shop. Check --shop or use a token for that shop.", 2);
43
40
  }
44
41
  }
@@ -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;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"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/bundle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAoBpD,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;CAwEjC"}
@@ -7,6 +7,14 @@ import { bundleTask, displayTaskPath, resolveTaskSelector } from "../../tasks.js
7
7
  function looksLikeTaskPath(input) {
8
8
  return input.includes("/") || input.includes("\\") || input.toLowerCase().endsWith(".json");
9
9
  }
10
+ function stripTrailingSeparators(input) {
11
+ let result = input;
12
+ const root = path.parse(path.resolve(input)).root;
13
+ while (result.length > root.length && /[\\/]$/.test(result)) {
14
+ result = result.slice(0, -1);
15
+ }
16
+ return result;
17
+ }
10
18
  export default class TasksBundle extends BaseCommand {
11
19
  static summary = "Bundle one helper task directory into one task JSON file.";
12
20
  static description = "Bundle one local task slug or editable helper task directory into one canonical task JSON file.";
@@ -55,15 +63,16 @@ export default class TasksBundle extends BaseCommand {
55
63
  this.log(`Bundled ${this.taskName(inputDisplay)} -> ${this.taskName(outputDisplay)}`);
56
64
  }
57
65
  async resolveBundlePaths(input, out) {
58
- const inputPath = path.resolve(input);
66
+ const normalizedInput = stripTrailingSeparators(input);
67
+ const inputPath = path.resolve(normalizedInput);
59
68
  if (await pathExists(path.join(inputPath, "task.json"))) {
60
69
  return {
61
70
  dirPath: inputPath,
62
- inputDisplay: input,
63
- outputDisplay: out || `${input}.json`,
71
+ inputDisplay: normalizedInput,
72
+ outputDisplay: out || `${normalizedInput}.json`,
64
73
  };
65
74
  }
66
- if (!looksLikeTaskPath(input)) {
75
+ if (!looksLikeTaskPath(normalizedInput)) {
67
76
  let project = null;
68
77
  try {
69
78
  project = await this.loadProject();
@@ -91,14 +100,14 @@ export default class TasksBundle extends BaseCommand {
91
100
  }
92
101
  }
93
102
  }
94
- if (!input.toLowerCase().endsWith(".json")) {
103
+ if (!normalizedInput.toLowerCase().endsWith(".json")) {
95
104
  return {
96
105
  dirPath: inputPath,
97
- inputDisplay: input,
98
- outputDisplay: out || `${input}.json`,
106
+ inputDisplay: normalizedInput,
107
+ outputDisplay: out || `${normalizedInput}.json`,
99
108
  };
100
109
  }
101
- const helperInput = input.slice(0, -".json".length);
110
+ const helperInput = normalizedInput.slice(0, -".json".length);
102
111
  const helperPath = path.resolve(helperInput);
103
112
  if (!(await pathExists(path.join(helperPath, "task.json")))) {
104
113
  throw new CliError([
@@ -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;CAoH3B"}
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/diff.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAapD,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;CAuI3B"}
@@ -1,7 +1,7 @@
1
1
  import { Args, Flags } from "@oclif/core";
2
2
  import { BaseCommand } from "../../base-command.js";
3
3
  import { linkForSlug } from "../../config.js";
4
- import { diffTasks, displayTaskPath, readTaskFile, remoteChangedSinceLastPull, selectedTaskFiles, slugFromTaskFile, taskForPush, } from "../../tasks.js";
4
+ import { diffTasks, displayTaskPath, localChangedSinceLastSync, readTaskFile, remoteChangedSinceLastPull, selectedTaskFiles, slugFromTaskFile, taskForPush, } from "../../tasks.js";
5
5
  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.";
@@ -44,11 +44,13 @@ export default class TasksDiff extends BaseCommand {
44
44
  const results = [];
45
45
  let hasFieldDifferences = false;
46
46
  let hasRemoteChanges = false;
47
+ let hasUnlinkedTasks = false;
47
48
  for (const file of files) {
48
49
  const slug = slugFromTaskFile(file);
49
50
  const link = linkForSlug(project, slug);
50
51
  const relativeFile = displayTaskPath(project, file);
51
52
  if (!link) {
53
+ hasUnlinkedTasks = true;
52
54
  results.push({
53
55
  file: relativeFile,
54
56
  slug,
@@ -67,6 +69,7 @@ export default class TasksDiff extends BaseCommand {
67
69
  const remoteTask = taskForPush(remote.task);
68
70
  const diff = diffTasks(localTask, remoteTask);
69
71
  const remoteChanged = remoteChangedSinceLastPull(link, remote);
72
+ const localChanged = localChangedSinceLastSync(link, localTask);
70
73
  const hasDiff = Boolean(diff);
71
74
  if (remoteChanged) {
72
75
  hasRemoteChanges = true;
@@ -86,13 +89,23 @@ export default class TasksDiff extends BaseCommand {
86
89
  if (remoteChanged || diff) {
87
90
  const section = [`# ${relativeFile}`];
88
91
  if (remoteChanged) {
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.`), "");
92
+ section.push(this.color("yellow", localChanged
93
+ ? "Mechanic and your local file both changed since this file was last synced."
94
+ : "Mechanic changed since this file was last synced."), this.muted("Showing current Mechanic compared with your local file."), this.muted("Diff legend: - current Mechanic, + local file."), ...(localChanged ? [
95
+ this.muted("This compares the current versions; it does not show edit history."),
96
+ this.muted("A normal pull will stop because local changes would be overwritten."),
97
+ this.muted(`Run "mechanic tasks pull ${link.remote_id} --force" only if current Mechanic should replace your local file.`),
98
+ this.muted(`Run "mechanic tasks publish ${relativeFile} --force" only if your local file should overwrite Mechanic.`),
99
+ ] : [
100
+ this.muted("Your local file has no unsynced changes."),
101
+ this.muted(`Run "mechanic tasks pull ${link.remote_id}" to update your local file from current Mechanic.`),
102
+ ]), "");
90
103
  }
91
104
  if (diff) {
92
105
  section.push(diff);
93
106
  }
94
107
  else {
95
- section.push("No field differences between the current remote task and your local file.");
108
+ section.push("No field differences between current Mechanic and your local file.");
96
109
  }
97
110
  differences.push(this.colorDiff(section.join("\n")));
98
111
  }
@@ -116,7 +129,14 @@ export default class TasksDiff extends BaseCommand {
116
129
  return;
117
130
  }
118
131
  this.log(differences.join("\n\n"));
119
- this.log(hasFieldDifferences ? "Differences found." : "Remote changes found.");
132
+ let summary = "Differences found.";
133
+ if (!hasFieldDifferences && hasRemoteChanges) {
134
+ summary = "Remote changes found.";
135
+ }
136
+ else if (!hasFieldDifferences && hasUnlinkedTasks) {
137
+ summary = "Unlinked tasks found.";
138
+ }
139
+ this.log(summary);
120
140
  if (flags["exit-code"]) {
121
141
  process.exitCode = 1;
122
142
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/pull.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAoBpD,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAChD,OAAgB,OAAO,SAAmD;IAC1E,OAAgB,WAAW,SAAmI;IAE9J,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;MAGnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAsG3B"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/pull.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAoBpD,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAChD,OAAgB,OAAO,SAAmD;IAC1E,OAAgB,WAAW,SAAmI;IAE9J,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;MAGnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAuG3B"}
@@ -68,13 +68,13 @@ export default class TasksPull extends BaseCommand {
68
68
  throw new CliError([
69
69
  `${displayTaskPath(project, filePath)} has local changes that would be overwritten.`,
70
70
  `Run "mechanic tasks diff ${displayTaskPath(project, filePath)}" to review them.`,
71
- `Re-run "mechanic tasks pull ${envelope.id} --force" only if the remote version should replace the local file.`,
71
+ `Re-run "mechanic tasks pull ${envelope.id} --force" only if current Mechanic should replace your local file.`,
72
72
  ].join("\n"), 2);
73
73
  }
74
74
  }
75
75
  }
76
76
  await writeTaskFilePathAndRefreshHelper(filePath, envelope.task);
77
- links = updateLink({ ...project, links }, slug, envelope.id, envelope.content_hash, contentHash(taskForPush(envelope.task)));
77
+ links = updateLink({ ...project, links }, slug, envelope.id, envelope.content_hash, contentHash(taskForPush(envelope.task)), filePath);
78
78
  await saveLinks(project, links);
79
79
  rows.push([
80
80
  this.taskId(project, envelope.id),
@@ -1 +1 @@
1
- {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/push.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../../src/commands/tasks/push.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@ import { linkForSlug, saveLinks, taskAdminUrl, updateLink } from "../../config.j
5
5
  import { CliError } from "../../errors.js";
6
6
  import { contentHash } from "../../hash.js";
7
7
  import { stableStringify } from "../../json.js";
8
- import { displayTaskPath, remoteChangedSinceLastPull, remoteChangedSinceLastPullDetails, remoteChangedSinceLastPullMessage, selectedTaskFiles, readRawTaskFile, slugFromTaskFile, taskForPush, taskSlug, unbundledHelperDirForTaskFile, validateTaskForPush, writeTaskFilePathAndRefreshHelper, } from "../../tasks.js";
8
+ import { displayTaskPath, localChangedSinceLastSync, remoteChangedSinceLastPull, remoteChangedSinceLastPullDetails, remoteChangedSinceLastPullMessage, selectedTaskFiles, readRawTaskFile, slugFromTaskFile, taskForPush, taskSlug, unbundledHelperDirForTaskFile, validateTaskForPush, writeTaskFilePathAndRefreshHelper, } from "../../tasks.js";
9
9
  function createTaskIdempotencyKey(shopDomain, slug) {
10
10
  const hex = createHash("sha256")
11
11
  .update(`mechanic-cli-task-create\0${shopDomain}\0${slug}`)
@@ -82,7 +82,7 @@ export default class TasksPush extends BaseCommand {
82
82
  const link = linkForSlug({ ...project, links }, slug);
83
83
  if (!link) {
84
84
  const created = await client.createTask(task, createTaskIdempotencyKey(project.shopDomain, slug));
85
- links = updateLink({ ...project, links }, slug, created.id, created.content_hash, contentHash(taskForPush(created.task)));
85
+ links = updateLink({ ...project, links }, slug, created.id, created.content_hash, contentHash(taskForPush(created.task)), file);
86
86
  await saveLinks(project, links);
87
87
  await writeTaskFilePathAndRefreshHelper(file, created.task);
88
88
  rows.push({
@@ -112,7 +112,7 @@ export default class TasksPush extends BaseCommand {
112
112
  task,
113
113
  ...(force ? { force: true } : { previous_content_hash: link.last_remote_content_hash }),
114
114
  });
115
- links = updateLink({ ...project, links }, slug, updated.id, updated.content_hash, contentHash(taskForPush(updated.task)));
115
+ links = updateLink({ ...project, links }, slug, updated.id, updated.content_hash, contentHash(taskForPush(updated.task)), file);
116
116
  await saveLinks(project, links);
117
117
  await writeTaskFilePathAndRefreshHelper(file, updated.task);
118
118
  rows.push({
@@ -129,7 +129,7 @@ export default class TasksPush extends BaseCommand {
129
129
  async preflightRemoteChanges(client, project, links, preparedTasks) {
130
130
  const remoteBySlug = new Map();
131
131
  const conflicts = [];
132
- for (const { slug, relativeFile } of preparedTasks) {
132
+ for (const { slug, relativeFile, task } of preparedTasks) {
133
133
  const link = linkForSlug({ ...project, links }, slug);
134
134
  if (!link) {
135
135
  continue;
@@ -137,12 +137,12 @@ export default class TasksPush extends BaseCommand {
137
137
  const remote = await client.getTask(link.remote_id);
138
138
  remoteBySlug.set(slug, remote);
139
139
  if (remoteChangedSinceLastPull(link, remote)) {
140
- conflicts.push(remoteChangedSinceLastPullMessage(relativeFile, link.remote_id));
140
+ conflicts.push(remoteChangedSinceLastPullMessage(relativeFile, link.remote_id, localChangedSinceLastSync(link, task)));
141
141
  }
142
142
  }
143
143
  if (conflicts.length > 0) {
144
144
  throw new CliError([
145
- "Remote changes found before publishing. Nothing was published.",
145
+ "Mechanic changes found before publishing. Nothing was published.",
146
146
  "",
147
147
  ...conflicts.map((conflict) => `- ${conflict}`),
148
148
  ].join("\n"), 2);
package/dist/config.d.ts CHANGED
@@ -11,5 +11,5 @@ export declare function loadProject(cwd?: string): Promise<Project>;
11
11
  export declare function saveLinks(project: Project, links: LinksFile): Promise<void>;
12
12
  export declare function linkForSlug(project: Project, slug: string): LinkEntry | null;
13
13
  export declare function slugForRemoteId(project: Project, remoteId: string): string | null;
14
- export declare function updateLink(project: Project, slug: string, remoteId: string, contentHash: string, localContentHash?: string): LinksFile;
14
+ export declare function updateLink(project: Project, slug: string, remoteId: string, contentHash: string, localContentHash?: string, filePath?: string): LinksFile;
15
15
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEhF,eAAO,MAAM,oBAAoB,QAAkE,CAAC;AA0BpG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAyBzD;AA+BD,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,SAA4B,GAAG,MAAM,CAG9F;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,UAAU,SAAuB,EACjC,MAAM,SAA4D,GACjE,cAAc,CAOhB;AAED,wBAAgB,UAAU,IAAI,SAAS,CAEtC;AAwED,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,UAAQ,GACZ,OAAO,CAAC,OAAO,CAAC,CAyBlB;AAED,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA0BhF;AAED,wBAAsB,WAAW,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAkCvE;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAGjF;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAE5E;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEjF;AAED,wBAAgB,UAAU,CACxB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,gBAAgB,CAAC,EAAE,MAAM,GACxB,SAAS,CAiBX"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEhF,eAAO,MAAM,oBAAoB,QAAkE,CAAC;AA0BpG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAyBzD;AA+BD,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,SAA4B,GAAG,MAAM,CAG9F;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,UAAU,SAAuB,EACjC,MAAM,SAA4D,GACjE,cAAc,CAOhB;AAED,wBAAgB,UAAU,IAAI,SAAS,CAEtC;AA6ED,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,UAAQ,GACZ,OAAO,CAAC,OAAO,CAAC,CA2BlB;AAED,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA0BhF;AAED,wBAAsB,WAAW,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAkCvE;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAGjF;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAE5E;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEjF;AAyBD,wBAAgB,UAAU,CACxB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,gBAAgB,CAAC,EAAE,MAAM,EACzB,QAAQ,CAAC,EAAE,MAAM,GAChB,SAAS,CAmBX"}
package/dist/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import nodeFs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { CliError } from "./errors.js";
3
4
  import { ensureDir, pathExists, readJson, readText, writeJson, writeText } from "./fs.js";
@@ -87,12 +88,12 @@ export function defaultConfig(shopDomain, apiBaseUrl = DEFAULT_API_BASE_URL, app
87
88
  export function emptyLinks() {
88
89
  return { tasks: {} };
89
90
  }
90
- function normalizeLinkEntry(slug, entry) {
91
+ function normalizeLinkEntry(slug, entry, tasksDirName) {
91
92
  if (!entry || typeof entry !== "object" || !entry.remote_id || !entry.last_remote_content_hash) {
92
93
  return null;
93
94
  }
94
95
  const normalized = {
95
- file: entry.file || `${DEFAULT_TASKS_DIR}/${slug}.json`,
96
+ file: entry.file || `${tasksDirName}/${slug}.json`,
96
97
  remote_id: entry.remote_id,
97
98
  last_remote_content_hash: entry.last_remote_content_hash,
98
99
  };
@@ -101,17 +102,17 @@ function normalizeLinkEntry(slug, entry) {
101
102
  }
102
103
  return [slug, normalized];
103
104
  }
104
- function normalizeLegacyLinkEntry(slug, entry) {
105
+ function normalizeLegacyLinkEntry(slug, entry, tasksDirName) {
105
106
  if (!entry || typeof entry !== "object" || !entry.task_id || !entry.last_remote_hash) {
106
107
  return null;
107
108
  }
108
109
  return [slug, {
109
- file: `${DEFAULT_TASKS_DIR}/${slug}.json`,
110
+ file: `${tasksDirName}/${slug}.json`,
110
111
  remote_id: entry.task_id,
111
112
  last_remote_content_hash: entry.last_remote_hash,
112
113
  }];
113
114
  }
114
- function normalizeLinks(value, shopDomain) {
115
+ function normalizeLinks(value, shopDomain, tasksDirName = DEFAULT_TASKS_DIR) {
115
116
  if (!value || typeof value !== "object") {
116
117
  return emptyLinks();
117
118
  }
@@ -119,7 +120,7 @@ function normalizeLinks(value, shopDomain) {
119
120
  if (links.tasks && typeof links.tasks === "object") {
120
121
  return {
121
122
  tasks: Object.fromEntries(Object.entries(links.tasks).flatMap(([slug, entry]) => {
122
- const normalized = normalizeLinkEntry(slug, entry);
123
+ const normalized = normalizeLinkEntry(slug, entry, tasksDirName);
123
124
  return normalized ? [normalized] : [];
124
125
  })),
125
126
  };
@@ -128,7 +129,7 @@ function normalizeLinks(value, shopDomain) {
128
129
  if (shopLinks && typeof shopLinks === "object") {
129
130
  return {
130
131
  tasks: Object.fromEntries(Object.entries(shopLinks).flatMap(([slug, entry]) => {
131
- const normalized = normalizeLegacyLinkEntry(slug, entry);
132
+ const normalized = normalizeLegacyLinkEntry(slug, entry, tasksDirName);
132
133
  return normalized ? [normalized] : [];
133
134
  })),
134
135
  };
@@ -147,7 +148,9 @@ export async function initProject(cwd, shopDomain, apiBaseUrl, appUrl, force = f
147
148
  }
148
149
  const existingConfig = force ? await readJson(configPath, {}) : {};
149
150
  const preserveLinks = force && existingConfig.shop_domain === shopDomain;
150
- const links = preserveLinks ? normalizeLinks(await readJson(linksPath, emptyLinks()), shopDomain) : emptyLinks();
151
+ const links = preserveLinks
152
+ ? normalizeLinks(await readJson(linksPath, emptyLinks()), shopDomain, config.tasks_dir)
153
+ : emptyLinks();
151
154
  config.api_base_url = normalizeApiBaseUrl(config.api_base_url);
152
155
  config.app_url = normalizeAppUrl(config.app_url || defaultAppUrl(config.shop_domain));
153
156
  await ensureDir(path.join(cwd, config.tasks_dir));
@@ -192,7 +195,7 @@ export async function loadProject(cwd = process.cwd()) {
192
195
  }
193
196
  const tasksDirName = config.tasks_dir || DEFAULT_TASKS_DIR;
194
197
  const linksPath = path.join(cwd, ".mechanic", "links.json");
195
- const links = normalizeLinks(await readJson(linksPath, emptyLinks()), config.shop_domain);
198
+ const links = normalizeLinks(await readJson(linksPath, emptyLinks()), config.shop_domain, tasksDirName);
196
199
  const apiBaseUrl = normalizeApiBaseUrl(process.env.MECHANIC_API_BASE_URL || config.api_base_url);
197
200
  const appUrl = normalizeAppUrl(process.env.MECHANIC_APP_URL || config.app_url || defaultAppUrl(config.shop_domain));
198
201
  return {
@@ -217,9 +220,30 @@ export function linkForSlug(project, slug) {
217
220
  export function slugForRemoteId(project, remoteId) {
218
221
  return Object.entries(project.links.tasks).find(([, link]) => link.remote_id === remoteId)?.[0] || null;
219
222
  }
220
- export function updateLink(project, slug, remoteId, contentHash, localContentHash) {
223
+ function realpathIfExists(filePath) {
224
+ try {
225
+ return nodeFs.realpathSync.native(filePath);
226
+ }
227
+ catch {
228
+ return filePath;
229
+ }
230
+ }
231
+ function relativeProjectPath(project, filePath) {
232
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(project.cwd, filePath);
233
+ const relativePath = path.relative(project.cwd, absolutePath);
234
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
235
+ return relativePath.split(path.sep).join("/");
236
+ }
237
+ const realProjectCwd = realpathIfExists(project.cwd);
238
+ const realFilePath = realpathIfExists(absolutePath);
239
+ const realRelativePath = path.relative(realProjectCwd, realFilePath);
240
+ return realRelativePath.split(path.sep).join("/");
241
+ }
242
+ export function updateLink(project, slug, remoteId, contentHash, localContentHash, filePath) {
243
+ const existingFile = project.links.tasks[slug]?.file;
244
+ const file = filePath ? relativeProjectPath(project, filePath) : existingFile || `${project.tasksDirName}/${slug}.json`;
221
245
  const entry = {
222
- file: `${project.tasksDirName}/${slug}.json`,
246
+ file,
223
247
  remote_id: remoteId,
224
248
  last_remote_content_hash: contentHash,
225
249
  };
package/dist/tasks.d.ts CHANGED
@@ -26,8 +26,9 @@ export declare function writeTaskFile(project: Project, slug: string, task: Json
26
26
  export declare function resolveTaskSelector(project: Project, input: string, options?: ResolveTaskSelectorOptions): Promise<ResolvedTaskSelector>;
27
27
  export declare function selectedTaskFiles(project: Project, input: string | undefined, all: boolean): Promise<string[]>;
28
28
  export declare function remoteChangedSinceLastPull(link: LinkEntry, remote: Pick<TaskEnvelope, "content_hash">): boolean;
29
+ export declare function localChangedSinceLastSync(link: LinkEntry, task: JsonObject): boolean;
29
30
  export declare function remoteChangedSinceLastPullDetails(force?: boolean): string;
30
- export declare function remoteChangedSinceLastPullMessage(relativeFile: string, remoteId: string): string;
31
+ export declare function remoteChangedSinceLastPullMessage(relativeFile: string, remoteId: string, localChanged?: boolean): string;
31
32
  export declare function taskForPush(task: JsonObject): JsonObject;
32
33
  export declare function nextAvailableSlug(project: Project, baseSlug: string): Promise<string>;
33
34
  export declare function taskFromHelperDir(dirPath: string): Promise<JsonObject>;
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAkB/E,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,kBAAkB,GAAG,MAAM,GAAG,WAAW,CAAC;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAIF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnE;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/D;AAMD,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiB1E;AAmBD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAExE;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ3E;AA0BD,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAG3F;AAED,wBAAsB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMjF;AAED,wBAAsB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAI3G;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAQpH;AAyKD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,oBAAoB,CAAC,CAuC/B;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAiDpH;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,cAAc,CAAC,GAAG,OAAO,CAE/G;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAOpF;AAED,wBAAgB,iCAAiC,CAAC,KAAK,UAAQ,GAAG,MAAM,CAEvE;AAED,wBAAgB,iCAAiC,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,MAAM,CAMtH;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAGxD;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAW3F;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAE5E;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMpE;AAED,wBAAsB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAS9G;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkB/E;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBlF;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhF;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CA4D/E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAEtF;AA2ID,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAyB9E"}
package/dist/tasks.js CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { CliError } from "./errors.js";
4
4
  import { ensureDir, pathExists, readJson, readText, removeFile, writeJson, writeText } from "./fs.js";
5
+ import { contentHash } from "./hash.js";
5
6
  import { stableStringify } from "./json.js";
6
7
  const SPLIT_FILES = [
7
8
  ["script", "script.liquid"],
@@ -318,11 +319,19 @@ export async function selectedTaskFiles(project, input, all) {
318
319
  export function remoteChangedSinceLastPull(link, remote) {
319
320
  return remote.content_hash !== link.last_remote_content_hash;
320
321
  }
322
+ export function localChangedSinceLastSync(link, task) {
323
+ const localHash = contentHash(taskForPush(task));
324
+ return (localHash !== link.last_remote_content_hash
325
+ && localHash !== link.last_local_content_hash);
326
+ }
321
327
  export function remoteChangedSinceLastPullDetails(force = false) {
322
- return force ? "remote changed since last pull; --force set" : "remote changed since last pull";
328
+ return force ? "Mechanic changed since last sync; --force set" : "Mechanic changed since last sync";
323
329
  }
324
- export function remoteChangedSinceLastPullMessage(relativeFile, remoteId) {
325
- return `${relativeFile} has changes in Mechanic since the last pull. Run "mechanic tasks diff ${relativeFile}" to review, then "mechanic tasks pull ${remoteId}" to keep the Mechanic version, or re-run with --force only if the local file should win.`;
330
+ export function remoteChangedSinceLastPullMessage(relativeFile, remoteId, localChanged = false) {
331
+ if (localChanged) {
332
+ return `${relativeFile} has changes in Mechanic and local changes since this file was last synced. Run "mechanic tasks diff ${relativeFile}" to review. Then run "mechanic tasks pull ${remoteId} --force" only if current Mechanic should replace your local file, or run "mechanic tasks publish ${relativeFile} --force" only if your local file should overwrite Mechanic.`;
333
+ }
334
+ return `${relativeFile} has changes in Mechanic since this file was last synced. Run "mechanic tasks diff ${relativeFile}" to review, then "mechanic tasks pull ${remoteId}" to update your local file from current Mechanic before publishing.`;
326
335
  }
327
336
  export function taskForPush(task) {
328
337
  const { enabled: _enabled, id: _id, remote_id: _remoteId, ...payload } = task;
@@ -439,6 +448,7 @@ export function validateTask(task, source) {
439
448
  export function validateTaskForPush(task, source) {
440
449
  return validateTask(task, source);
441
450
  }
451
+ const MAX_LINE_DIFF_CELLS = 250_000;
442
452
  function comparableTaskValue(value) {
443
453
  return value === undefined ? "(missing)" : String(stableStringify(value));
444
454
  }
@@ -489,6 +499,14 @@ function lineDiff(remoteLines, localLines) {
489
499
  return operations;
490
500
  }
491
501
  function formattedLineDiff(remoteLines, localLines) {
502
+ if ((remoteLines.length + 1) * (localLines.length + 1) > MAX_LINE_DIFF_CELLS) {
503
+ return [
504
+ "@@",
505
+ `- ${remoteLines.length} current Mechanic lines`,
506
+ `+ ${localLines.length} local file lines`,
507
+ "Large value changed; detailed line diff omitted to keep the CLI responsive.",
508
+ ];
509
+ }
492
510
  const operations = lineDiff(remoteLines, localLines);
493
511
  const changeIndexes = operations.flatMap((operation, index) => (operation.kind === "context" ? [] : [index]));
494
512
  if (changeIndexes.length === 0) {
@@ -540,7 +558,7 @@ export function diffTasks(local, remote) {
540
558
  }
541
559
  const sections = [`Changed fields: ${changedFields.join(", ")}`];
542
560
  for (const field of changedFields) {
543
- sections.push("", `## ${field}`, "--- remote", "+++ local", ...formattedValueDiff(comparableRemote[field], comparableLocal[field]));
561
+ sections.push("", `## ${field}`, "--- current Mechanic", "+++ local file", ...formattedValueDiff(comparableRemote[field], comparableLocal[field]));
544
562
  }
545
563
  return sections.join("\n");
546
564
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightward/mechanic-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.8",
4
4
  "description": "Develop, preview, diff, and publish Mechanic Shopify automation tasks from local files",
5
5
  "type": "module",
6
6
  "keywords": [