@frontera-sdk/cli 1.50.21 → 1.50.23

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": "@frontera-sdk/cli",
3
- "version": "1.50.21",
3
+ "version": "1.50.23",
4
4
  "description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -39,14 +39,14 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@anthropic-ai/claude-agent-sdk": "^0.3.251",
42
- "@frontera-sdk/functions": "1.50.21",
43
- "@frontera-sdk/core": "1.50.21",
42
+ "@frontera-sdk/functions": "1.50.23",
43
+ "@frontera-sdk/core": "1.50.23",
44
44
  "ai": "^6.0.116",
45
45
  "gray-matter": "^4.0.3",
46
46
  "yaml": "^2.9.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@frontera-sdk/forge-contracts": "1.50.21",
49
+ "@frontera-sdk/forge-contracts": "1.50.23",
50
50
  "@types/bun": "^1.3.14",
51
51
  "typescript": "^5.9.3"
52
52
  }
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, statSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
4
  import { AppsApi } from '../../api/apps-api'
@@ -41,12 +41,39 @@ async function publish(
41
41
  }
42
42
  }
43
43
 
44
+
45
+ /**
46
+ * Newest modification time across a set of project-relative files.
47
+ *
48
+ * `statSync` per file rather than one directory stat: a build writes into an
49
+ * existing directory, so the directory's own mtime says nothing about whether
50
+ * the files inside it are current.
51
+ */
52
+ function newestMtime(root: string, files: Array<{ rel: string }>): { at: number; rel: string } {
53
+ let at = 0
54
+ let rel = ''
55
+ for (const file of files) {
56
+ try {
57
+ const stat = statSync(join(root, file.rel))
58
+ if (stat.mtimeMs > at) {
59
+ at = stat.mtimeMs
60
+ rel = file.rel
61
+ }
62
+ } catch {
63
+ // A file that vanished between listing and stat is not evidence of
64
+ // anything; the comparison below is a warning about staleness, not a
65
+ // filesystem audit.
66
+ }
67
+ }
68
+ return { at, rel }
69
+ }
70
+
44
71
  export const appDeploy: Command = {
45
72
  meta: {
46
73
  noun: 'app',
47
74
  verb: 'deploy',
48
75
  args: [],
49
- flags: { version: 'string', 'no-promote': 'boolean' },
76
+ flags: { version: 'string', 'no-promote': 'boolean', 'skip-build-check': 'boolean' },
50
77
  summary: 'Build output up to an immutable version, and promote it',
51
78
  examples: ['frontera app deploy', 'frontera app deploy --no-promote'],
52
79
  needsProject: true,
@@ -66,10 +93,49 @@ export const appDeploy: Command = {
66
93
  })
67
94
  }
68
95
 
96
+ const distFiles = collectSourceFiles(distDir)
97
+
98
+ /**
99
+ * Refuse a bundle older than the source it claims to be built from.
100
+ *
101
+ * `deploy` packages `dist/`; it does not build. So an edit made after the
102
+ * last build ships the PREVIOUS bundle — and nothing about that failure is
103
+ * visible: the command succeeds, a version is published, promoting it
104
+ * works, and the user is looking at yesterday's app while everyone
105
+ * involved believes the change went out. That silence is what makes it
106
+ * worth a hard stop rather than a warning.
107
+ *
108
+ * Compared against the newest file on each side, not file-by-file: a build
109
+ * legitimately rewrites only what changed, so an untouched chunk being
110
+ * older than its source proves nothing.
111
+ */
112
+ if (!flagBool(ctx, 'skip-build-check')) {
113
+ // `.frontera/` is this machine's notes about the tree — render evidence,
114
+ // pulled-version state — written by tooling AFTER a build and never an
115
+ // input to one. Counting it would make the check fire on itself: render
116
+ // the app, then deploy, and the render's own receipt is the newest file.
117
+ const sources = collectSourceFiles(project.root).filter(
118
+ (file) => !file.rel.startsWith('.frontera/'),
119
+ )
120
+ const newestSource = newestMtime(project.root, sources)
121
+ const newestDist = newestMtime(distDir, distFiles)
122
+ if (newestSource.at > newestDist.at) {
123
+ throw new CliError(
124
+ `${outputDirectory}/ is older than your source — ${newestSource.rel} changed after the last build`,
125
+ {
126
+ code: 'VALIDATION_ERROR',
127
+ hint:
128
+ 'run your build first: bun run build. '
129
+ + 'Deploying now would publish the previous bundle, and nothing would say so. '
130
+ + 'Pass --skip-build-check if the output really is current.',
131
+ },
132
+ )
133
+ }
134
+ }
135
+
69
136
  const client = new AppsApi(ctx.apiUrl, ctx.token)
70
137
  const app = await client.ensureApp(project.slug, project.displayName, project.appId)
71
138
 
72
- const distFiles = collectSourceFiles(distDir)
73
139
  ctx.output.note(
74
140
  ` bundle ${distFiles.length} files, ${formatBytes(distFiles.reduce((n, f) => n + f.bytes.byteLength, 0))}`,
75
141
  )
package/src/flag-help.ts CHANGED
@@ -86,6 +86,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
86
86
  // flag spends no characters on the `{"pk": …}` the route models it as.
87
87
  subject: 'primary key of the object a subject-bound workflow runs for',
88
88
  'no-promote': 'publish the version without moving the live pointer',
89
+ 'skip-build-check': 'deploy even when the build output is older than the source',
89
90
  'no-wait': 'return as soon as the run is queued instead of waiting for it to finish',
90
91
  // Named for what it RUNS, not where it runs: an author reads this to decide
91
92
  // between their working copy and what is deployed.
@@ -113,6 +113,7 @@ export const BOOLEAN_FLAGS = new Set([
113
113
  'accept-contract-change', 'accept-orphaned-knowledge', 'dry-run', 'end-user', 'no-activate',
114
114
  'no-components', 'no-git', 'no-input', 'no-install', 'no-open', 'no-promote', 'no-wait',
115
115
  'remove-apps', 'token-stdin', 'prune', 'dev', 'revoke', 'draft', 'check',
116
+ 'skip-build-check',
116
117
  ])
117
118
 
118
119
  /**