@frontera-sdk/cli 1.50.22 → 1.50.24
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 +4 -4
- package/src/commands/app/deploy.ts +69 -3
- package/src/flag-help.ts +1 -0
- package/src/forge/touches.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.24",
|
|
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.
|
|
43
|
-
"@frontera-sdk/core": "1.50.
|
|
42
|
+
"@frontera-sdk/functions": "1.50.24",
|
|
43
|
+
"@frontera-sdk/core": "1.50.24",
|
|
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.
|
|
49
|
+
"@frontera-sdk/forge-contracts": "1.50.24",
|
|
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.
|
package/src/forge/touches.ts
CHANGED
|
@@ -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
|
/**
|