@biznetgio/neo-app 0.4.47 → 0.4.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.47",
3
+ "version": "0.4.49",
4
4
  "description": "NEO App CLI — one-command agent onboarding, MCP server, and project ops for coding agents (Codex, Claude Code).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,7 +54,9 @@ function collectExcludes(dir) {
54
54
  for (let line of readFileSync(p, 'utf8').split(/\r?\n/)) {
55
55
  line = line.trim();
56
56
  if (!line || line.startsWith('#') || line.startsWith('!')) continue;
57
- excludes.add(line.replace(/^\/+/, '').replace(/\/+$/, ''));
57
+ // Keep a leading slash: a gitignore anchor like `/themes` matches ONLY the root
58
+ // entry, not every nested `themes/` segment (JNG #116). Trailing slash is dropped.
59
+ excludes.add(line.replace(/\/+$/, ''));
58
60
  }
59
61
  }
60
62
  return excludes;
@@ -67,6 +69,13 @@ function matchesExclude(rel, excludes) {
67
69
  const parts = rel.split('/');
68
70
  const base = parts[parts.length - 1];
69
71
  for (const pat of excludes) {
72
+ if (pat.startsWith('/')) {
73
+ // Anchored (gitignore `/themes`): matches only at the repo root — the entry itself
74
+ // or something directly under it — NOT every nested `themes/` segment (JNG #116).
75
+ const a = pat.slice(1);
76
+ if (rel === a || rel.startsWith(a + '/')) return true;
77
+ continue;
78
+ }
70
79
  if (pat === base || pat === rel || parts.includes(pat)) return true;
71
80
  if (pat.startsWith('*.') && base.endsWith(pat.slice(1))) return true;
72
81
  }
@@ -84,6 +93,9 @@ function collectIncludedFiles(dir) {
84
93
  let entries;
85
94
  try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
86
95
  for (const e of entries) {
96
+ // Skip macOS cruft: AppleDouble resource forks (._*) and .DS_Store break Linux
97
+ // builds and are never part of the app (JNG #115).
98
+ if (e.name === '.DS_Store' || e.name.startsWith('._')) continue;
87
99
  const r = rel ? `${rel}/${e.name}` : e.name;
88
100
  if (matchesExclude(r, excludes) || isSecretPath(r)) continue;
89
101
  if (e.isDirectory()) walk(join(d, e.name), r);
@@ -267,7 +279,10 @@ export async function deploySource(api, { projectId, src, name, port, build, ser
267
279
  if (src.repo && app.repoUrl !== src.repo) patch.repoUrl = src.repo;
268
280
  if (src.branch && app.branch !== src.branch) patch.branch = src.branch;
269
281
  }
270
- if (buildType && app.buildType && app.buildType !== buildType) patch.buildType = buildType;
282
+ // Only change an existing service's build-type when EXPLICITLY asked (--build/build),
283
+ // never from an auto-detected buildType (e.g. a Dockerfile that appeared in the repo):
284
+ // a silent build-type switch on redeploy surprised users (JNG #131).
285
+ if (build && app.buildType && app.buildType !== buildType) patch.buildType = buildType;
271
286
  if (Object.keys(patch).length) {
272
287
  plan.push(`update "${svc}": ${Object.keys(patch).join(', ')}`);
273
288
  if (!previewOnly) {
@@ -337,7 +352,10 @@ export async function deploySource(api, { projectId, src, name, port, build, ser
337
352
  const listFile = join(tmpdir(), `neo-files-${app.id}-${Date.now()}.txt`);
338
353
  writeFileSync(listFile, files.join('\n') + '\n');
339
354
  try {
340
- execFileSync('tar', ['czf', archive, '-C', src.dir, '-T', listFile], { stdio: 'ignore' });
355
+ // COPYFILE_DISABLE=1 stops macOS bsdtar from SYNTHESIZING AppleDouble (._*)
356
+ // entries for files that carry extended attributes — those break Linux glob
357
+ // builds even though they aren't on disk (JNG #115).
358
+ execFileSync('tar', ['czf', archive, '-C', src.dir, '-T', listFile], { stdio: 'ignore', env: { ...process.env, COPYFILE_DISABLE: '1' } });
341
359
  } finally {
342
360
  rmSync(listFile, { force: true });
343
361
  }
package/src/mcp/tools.js CHANGED
@@ -1293,7 +1293,9 @@ export const TOOLS = [
1293
1293
  required: ['planId', 'planHash', 'confirmationId'],
1294
1294
  },
1295
1295
  async call(api, args) {
1296
- const { status, data } = await api.post(`/api/v1/plans/${args.planId}/apply`, { planHash: args.planHash, confirmationId: args.confirmationId, idempotencyKey: args.idempotencyKey });
1296
+ // Always send an idempotency key (reuse the same one on a retry to dedup/resume).
1297
+ const idempotencyKey = args.idempotencyKey || `idem-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1298
+ const { status, data } = await api.post(`/api/v1/plans/${args.planId}/apply`, { planHash: args.planHash, confirmationId: args.confirmationId, idempotencyKey });
1297
1299
  return redactSecrets(result(status, data));
1298
1300
  },
1299
1301
  },