@gadgetinc/ggt 0.4.10 → 1.0.0
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 +165 -93
- package/lib/__generated__/graphql.js +66 -1
- package/lib/__generated__/graphql.js.map +1 -1
- package/lib/commands/deploy.js +328 -230
- package/lib/commands/deploy.js.map +1 -1
- package/lib/commands/dev.js +445 -0
- package/lib/commands/dev.js.map +1 -0
- package/lib/commands/list.js +27 -19
- package/lib/commands/list.js.map +1 -1
- package/lib/commands/login.js +15 -11
- package/lib/commands/login.js.map +1 -1
- package/lib/commands/logout.js +5 -5
- package/lib/commands/logout.js.map +1 -1
- package/lib/commands/open.js +200 -0
- package/lib/commands/open.js.map +1 -0
- package/lib/commands/pull.js +128 -0
- package/lib/commands/pull.js.map +1 -0
- package/lib/commands/push.js +126 -0
- package/lib/commands/push.js.map +1 -0
- package/lib/commands/root.js +46 -28
- package/lib/commands/root.js.map +1 -1
- package/lib/commands/status.js +61 -0
- package/lib/commands/status.js.map +1 -0
- package/lib/commands/version.js +6 -6
- package/lib/commands/version.js.map +1 -1
- package/lib/commands/whoami.js +6 -6
- package/lib/commands/whoami.js.map +1 -1
- package/lib/ggt.js +33 -8
- package/lib/ggt.js.map +1 -1
- package/lib/main.js +5 -0
- package/lib/main.js.map +1 -0
- package/lib/services/app/api/api.js +191 -0
- package/lib/services/app/api/api.js.map +1 -0
- package/lib/services/app/api/operation.js +12 -0
- package/lib/services/app/api/operation.js.map +1 -0
- package/lib/services/app/app.js +44 -10
- package/lib/services/app/app.js.map +1 -1
- package/lib/services/app/{edit/client.js → client.js} +29 -19
- package/lib/services/app/client.js.map +1 -0
- package/lib/services/app/edit/edit.js +67 -31
- package/lib/services/app/edit/edit.js.map +1 -1
- package/lib/services/app/edit/operation.js +4 -3
- package/lib/services/app/edit/operation.js.map +1 -1
- package/lib/services/app/{edit/error.js → error.js} +6 -6
- package/lib/services/app/error.js.map +1 -0
- package/lib/services/command/arg.js +4 -4
- package/lib/services/command/arg.js.map +1 -1
- package/lib/services/command/command.js +9 -7
- package/lib/services/command/command.js.map +1 -1
- package/lib/services/command/context.js +82 -20
- package/lib/services/command/context.js.map +1 -1
- package/lib/services/config/config.js +4 -7
- package/lib/services/config/config.js.map +1 -1
- package/lib/services/config/env.js +1 -1
- package/lib/services/config/env.js.map +1 -1
- package/lib/services/filesync/changes.js +76 -37
- package/lib/services/filesync/changes.js.map +1 -1
- package/lib/services/filesync/conflicts.js +10 -9
- package/lib/services/filesync/conflicts.js.map +1 -1
- package/lib/services/filesync/directory.js +16 -1
- package/lib/services/filesync/directory.js.map +1 -1
- package/lib/services/filesync/error.js +96 -27
- package/lib/services/filesync/error.js.map +1 -1
- package/lib/services/filesync/filesync.js +448 -490
- package/lib/services/filesync/filesync.js.map +1 -1
- package/lib/services/filesync/hashes.js +8 -5
- package/lib/services/filesync/hashes.js.map +1 -1
- package/lib/services/filesync/strategy.js +59 -0
- package/lib/services/filesync/strategy.js.map +1 -0
- package/lib/services/filesync/sync-json.js +475 -0
- package/lib/services/filesync/sync-json.js.map +1 -0
- package/lib/services/http/auth.js +30 -1
- package/lib/services/http/auth.js.map +1 -1
- package/lib/services/http/http.js +5 -0
- package/lib/services/http/http.js.map +1 -1
- package/lib/services/output/confirm.js +149 -0
- package/lib/services/output/confirm.js.map +1 -0
- package/lib/services/output/footer.js +22 -0
- package/lib/services/output/footer.js.map +1 -0
- package/lib/services/output/log/format/pretty.js +2 -1
- package/lib/services/output/log/format/pretty.js.map +1 -1
- package/lib/services/output/log/logger.js +13 -5
- package/lib/services/output/log/logger.js.map +1 -1
- package/lib/services/output/log/structured.js +2 -2
- package/lib/services/output/log/structured.js.map +1 -1
- package/lib/services/output/output.js +197 -0
- package/lib/services/output/output.js.map +1 -0
- package/lib/services/output/print.js +31 -0
- package/lib/services/output/print.js.map +1 -0
- package/lib/services/output/problems.js +84 -0
- package/lib/services/output/problems.js.map +1 -0
- package/lib/services/output/prompt.js +173 -40
- package/lib/services/output/prompt.js.map +1 -1
- package/lib/services/output/report.js +63 -19
- package/lib/services/output/report.js.map +1 -1
- package/lib/services/output/select.js +198 -0
- package/lib/services/output/select.js.map +1 -0
- package/lib/services/output/spinner.js +141 -0
- package/lib/services/output/spinner.js.map +1 -0
- package/lib/services/output/sprint.js +38 -15
- package/lib/services/output/sprint.js.map +1 -1
- package/lib/services/output/symbols.js +23 -0
- package/lib/services/output/symbols.js.map +1 -0
- package/lib/services/output/table.js +98 -0
- package/lib/services/output/table.js.map +1 -0
- package/lib/services/output/timestamp.js +12 -0
- package/lib/services/output/timestamp.js.map +1 -0
- package/lib/services/output/update.js +29 -9
- package/lib/services/output/update.js.map +1 -1
- package/lib/services/user/session.js +4 -0
- package/lib/services/user/session.js.map +1 -1
- package/lib/services/user/user.js +15 -10
- package/lib/services/user/user.js.map +1 -1
- package/lib/services/util/assert.js +11 -0
- package/lib/services/util/assert.js.map +1 -0
- package/lib/services/util/boolean.js +2 -2
- package/lib/services/util/boolean.js.map +1 -1
- package/lib/services/util/function.js +45 -7
- package/lib/services/util/function.js.map +1 -1
- package/lib/services/util/is.js +23 -2
- package/lib/services/util/is.js.map +1 -1
- package/lib/services/util/json.js +16 -13
- package/lib/services/util/json.js.map +1 -1
- package/lib/services/util/object.js +2 -2
- package/lib/services/util/object.js.map +1 -1
- package/lib/services/util/promise.js +5 -2
- package/lib/services/util/promise.js.map +1 -1
- package/lib/services/util/types.js.map +1 -1
- package/npm-shrinkwrap.json +3415 -2973
- package/package.json +47 -40
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -14
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -5
- package/lib/commands/sync.js +0 -284
- package/lib/commands/sync.js.map +0 -1
- package/lib/services/app/edit/client.js.map +0 -1
- package/lib/services/app/edit/error.js.map +0 -1
- package/lib/services/output/log/printer.js +0 -120
- package/lib/services/output/log/printer.js.map +0 -1
- package/lib/services/output/stream.js +0 -54
- package/lib/services/output/stream.js.map +0 -1
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { _ as _define_property } from "@swc/helpers/_/_define_property";
|
|
2
|
+
import { findUp } from "find-up";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import assert from "node:assert";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { simpleGit } from "simple-git";
|
|
7
|
+
import terminalLink from "terminal-link";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { EnvironmentType, getApps } from "../app/app.js";
|
|
10
|
+
import { AppArg } from "../app/arg.js";
|
|
11
|
+
import { Edit } from "../app/edit/edit.js";
|
|
12
|
+
import { ArgError } from "../command/arg.js";
|
|
13
|
+
import { config, homePath } from "../config/config.js";
|
|
14
|
+
import { println } from "../output/print.js";
|
|
15
|
+
import { select } from "../output/select.js";
|
|
16
|
+
import { sprint, sprintln } from "../output/sprint.js";
|
|
17
|
+
import { getUserOrLogin } from "../user/user.js";
|
|
18
|
+
import { sortBySimilar } from "../util/collection.js";
|
|
19
|
+
import { defaults } from "../util/object.js";
|
|
20
|
+
import { Directory } from "./directory.js";
|
|
21
|
+
import { UnknownDirectoryError } from "./error.js";
|
|
22
|
+
export const SyncJsonArgs = {
|
|
23
|
+
"--app": {
|
|
24
|
+
type: AppArg,
|
|
25
|
+
alias: [
|
|
26
|
+
"-a",
|
|
27
|
+
"--application"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"--env": {
|
|
31
|
+
type: String,
|
|
32
|
+
alias: [
|
|
33
|
+
"-e",
|
|
34
|
+
"--environment"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"--allow-unknown-directory": Boolean,
|
|
38
|
+
"--allow-different-app": Boolean
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The state of the filesystem.
|
|
42
|
+
*
|
|
43
|
+
* This is persisted to `.gadget/sync.json` within the {@linkcode directory}.
|
|
44
|
+
*/ // TODO: rename and/or add to ctx?
|
|
45
|
+
export class SyncJson {
|
|
46
|
+
/**
|
|
47
|
+
* The last filesVersion that was written to the filesystem.
|
|
48
|
+
*
|
|
49
|
+
* This determines if the filesystem in Gadget is ahead of the
|
|
50
|
+
* filesystem on the local machine.
|
|
51
|
+
*/ get filesVersion() {
|
|
52
|
+
const environment = this.state.environments[this.state.environment];
|
|
53
|
+
assert(environment, "environment must exist in environments");
|
|
54
|
+
return BigInt(environment.filesVersion);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Loads a {@linkcode SyncJson} from the specified directory.
|
|
58
|
+
*
|
|
59
|
+
* Returns undefined if the directory doesn't exist, is empty, or
|
|
60
|
+
* doesn't contain a `.gadget/sync.json` file.
|
|
61
|
+
*/ static async load(ctx, { directory }) {
|
|
62
|
+
ctx = ctx.child({
|
|
63
|
+
name: "sync-json"
|
|
64
|
+
});
|
|
65
|
+
const user = await getUserOrLogin(ctx);
|
|
66
|
+
const availableApps = await getApps(ctx);
|
|
67
|
+
if (availableApps.length === 0) {
|
|
68
|
+
throw new ArgError(sprint`
|
|
69
|
+
You (${user.email}) don't have have any Gadget applications.
|
|
70
|
+
|
|
71
|
+
Visit https://gadget.new to create one!
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
// try to load the .gadget/sync.json file
|
|
75
|
+
const syncJsonFile = await fs.readFile(directory.absolute(".gadget/sync.json"), "utf8").catch((error)=>ctx.log.warn("failed to read .gadget/sync.json", {
|
|
76
|
+
error
|
|
77
|
+
}));
|
|
78
|
+
if (!syncJsonFile) {
|
|
79
|
+
// the .gadget/sync.json file doesn't exist in the directory
|
|
80
|
+
// or any of its parents
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
// the .gadget/sync.json file exists, try to parse it
|
|
84
|
+
let state;
|
|
85
|
+
try {
|
|
86
|
+
state = SyncJsonState.parse(JSON.parse(syncJsonFile));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// the .gadget/sync.json file exists, but it's invalid
|
|
89
|
+
ctx.log.warn("failed to parse .gadget/sync.json", {
|
|
90
|
+
error,
|
|
91
|
+
syncJsonFile
|
|
92
|
+
});
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
ctx.app = await loadApp(ctx, {
|
|
96
|
+
availableApps,
|
|
97
|
+
state
|
|
98
|
+
});
|
|
99
|
+
ctx.env = await loadEnv(ctx, {
|
|
100
|
+
app: ctx.app,
|
|
101
|
+
state
|
|
102
|
+
});
|
|
103
|
+
if (state.application !== ctx.app.slug) {
|
|
104
|
+
// .gadget/sync.json is associated with a different app
|
|
105
|
+
if (ctx.args["--allow-different-app"]) {
|
|
106
|
+
// the user passed --allow-different-app, so use the application
|
|
107
|
+
// and environment they specified and clobber everything
|
|
108
|
+
const syncJson = new SyncJson(ctx, directory, undefined, {
|
|
109
|
+
application: ctx.app.slug,
|
|
110
|
+
environment: ctx.env.name,
|
|
111
|
+
environments: {
|
|
112
|
+
[ctx.env.name]: {
|
|
113
|
+
filesVersion: "0"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
await syncJson.loadGitBranch();
|
|
118
|
+
return syncJson;
|
|
119
|
+
}
|
|
120
|
+
// the user didn't pass --allow-different-app, so throw an error
|
|
121
|
+
throw new ArgError(sprint`
|
|
122
|
+
You were about to sync the following app to the following directory:
|
|
123
|
+
|
|
124
|
+
${ctx.app.slug} (${ctx.env.name}) → ${directory.path}
|
|
125
|
+
|
|
126
|
+
However, that directory has already been synced with this app:
|
|
127
|
+
|
|
128
|
+
${state.application} (${state.environment})
|
|
129
|
+
|
|
130
|
+
If you're sure that you want to sync:
|
|
131
|
+
|
|
132
|
+
${ctx.app.slug} (${ctx.env.name}) → ${directory.path}
|
|
133
|
+
|
|
134
|
+
Run "ggt dev" with the {bold --allow-different-app} flag.
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
let previousEnvironment;
|
|
138
|
+
if (state.environment !== ctx.env.name) {
|
|
139
|
+
// the user specified a different environment, update the state
|
|
140
|
+
println({
|
|
141
|
+
ensureEmptyLineAbove: true
|
|
142
|
+
})`
|
|
143
|
+
Changing environment.
|
|
144
|
+
|
|
145
|
+
${state.environment} → ${ctx.env.name}
|
|
146
|
+
`;
|
|
147
|
+
previousEnvironment = state.environment;
|
|
148
|
+
state.environment = ctx.env.name;
|
|
149
|
+
if (!state.environments[ctx.env.name]) {
|
|
150
|
+
// the user has never synced to this environment before
|
|
151
|
+
state.environments[ctx.env.name] = {
|
|
152
|
+
filesVersion: "0"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const syncJson = new SyncJson(ctx, directory, previousEnvironment, state);
|
|
157
|
+
await syncJson.save(syncJson.filesVersion);
|
|
158
|
+
await syncJson.loadGitBranch();
|
|
159
|
+
return syncJson;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Loads a {@linkcode SyncJson} from the specified directory or
|
|
163
|
+
* initializes a new one.
|
|
164
|
+
*
|
|
165
|
+
* - Ensures a user is logged in.
|
|
166
|
+
* - Ensures the user has at least one application.
|
|
167
|
+
* - Ensures the directory exists.
|
|
168
|
+
* - Ensures the directory is empty or contains a `.gadget/sync.json` file, unless --allow-unknown-directory was passed
|
|
169
|
+
* - Ensures the specified app matches the app the directory previously synced to, unless --allow-different-app was passed
|
|
170
|
+
*/ // TODO: rename to loadOrAskAndInit
|
|
171
|
+
static async loadOrInit(ctx, { directory }) {
|
|
172
|
+
ctx = ctx.child({
|
|
173
|
+
name: "sync-json"
|
|
174
|
+
});
|
|
175
|
+
let syncJson = await SyncJson.load(ctx, {
|
|
176
|
+
directory
|
|
177
|
+
});
|
|
178
|
+
if (syncJson) {
|
|
179
|
+
// the .gadget/sync.json file already exists and is valid
|
|
180
|
+
return syncJson;
|
|
181
|
+
}
|
|
182
|
+
if (await directory.hasFiles() && !ctx.args["--allow-unknown-directory"]) {
|
|
183
|
+
// the directory isn't empty and the user didn't pass --allow-unknown-directory
|
|
184
|
+
throw new UnknownDirectoryError(ctx, {
|
|
185
|
+
directory
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
ctx.app = await loadApp(ctx, {
|
|
189
|
+
availableApps: await getApps(ctx)
|
|
190
|
+
});
|
|
191
|
+
ctx.env = await loadEnv(ctx, {
|
|
192
|
+
app: ctx.app
|
|
193
|
+
});
|
|
194
|
+
// the directory is empty or the user passed
|
|
195
|
+
// --allow-unknown-directory, either way ensure the directory exists
|
|
196
|
+
// and create a fresh .gadget/sync.json file
|
|
197
|
+
await fs.ensureDir(directory.path);
|
|
198
|
+
syncJson = new SyncJson(ctx, directory, undefined, {
|
|
199
|
+
application: ctx.app.slug,
|
|
200
|
+
environment: ctx.env.name,
|
|
201
|
+
environments: {
|
|
202
|
+
[ctx.env.name]: {
|
|
203
|
+
filesVersion: "0"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
await syncJson.save(syncJson.filesVersion);
|
|
208
|
+
await syncJson.loadGitBranch();
|
|
209
|
+
return syncJson;
|
|
210
|
+
}
|
|
211
|
+
// TODO: just asks the user to select an app and environment, doesn't create a .gadget/sync.json file
|
|
212
|
+
// static async loadOrAsk(ctx: Context<SyncJsonArgs>, { directory }: { directory: Directory }): Promise<SyncJson | undefined> {
|
|
213
|
+
// }
|
|
214
|
+
/**
|
|
215
|
+
* Updates {@linkcode _syncJson} and saves it to `.gadget/sync.json`.
|
|
216
|
+
*/ async save(filesVersion) {
|
|
217
|
+
const environment = this.state.environments[this.state.environment];
|
|
218
|
+
assert(environment, "environment must exist in environments");
|
|
219
|
+
environment.filesVersion = String(filesVersion);
|
|
220
|
+
this.ctx.log.debug("saving .gadget/sync.json");
|
|
221
|
+
this._mtime = Date.now();
|
|
222
|
+
await fs.outputJSON(this.directory.absolute(".gadget/sync.json"), {
|
|
223
|
+
...this.state,
|
|
224
|
+
// v0.4
|
|
225
|
+
app: this.state.application,
|
|
226
|
+
filesVersion: String(filesVersion),
|
|
227
|
+
mtime: this._mtime
|
|
228
|
+
}, {
|
|
229
|
+
spaces: 2
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async loadGitBranch() {
|
|
233
|
+
this.gitBranch = await loadBranch(this.ctx, {
|
|
234
|
+
directory: this.directory
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
sprint(options = {}) {
|
|
238
|
+
let str = sprintln`
|
|
239
|
+
Application ${this.app.slug}
|
|
240
|
+
Environment ${this.env.name}
|
|
241
|
+
`;
|
|
242
|
+
if (this.gitBranch) {
|
|
243
|
+
str += sprintln({
|
|
244
|
+
indent: 5
|
|
245
|
+
})`Branch ${this.gitBranch}`;
|
|
246
|
+
}
|
|
247
|
+
if (terminalLink.isSupported) {
|
|
248
|
+
str += sprintln({
|
|
249
|
+
ensureEmptyLineAbove: true
|
|
250
|
+
})`
|
|
251
|
+
${terminalLink("Preview", `https://${this.app.slug}--${this.env.name}.gadget.app`)} ${terminalLink("Editor", `https://${this.app.primaryDomain}/edit/${this.env.name}`)} ${terminalLink("Playground", `https://${this.app.primaryDomain}/api/playground/graphql?environment=${this.env.name}`)} ${terminalLink("Docs", `https://docs.gadget.dev/api/${this.app.slug}`)}
|
|
252
|
+
`;
|
|
253
|
+
} else {
|
|
254
|
+
str += sprintln`
|
|
255
|
+
------------------------
|
|
256
|
+
Preview https://${this.app.slug}--${this.env.name}.gadget.app
|
|
257
|
+
Editor https://${this.app.primaryDomain}/edit/${this.env.name}
|
|
258
|
+
Playground https://${this.app.primaryDomain}/api/playground/graphql?environment=${this.env.name}
|
|
259
|
+
Docs https://docs.gadget.dev/api/${this.app.slug}
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
return sprintln(options)(str);
|
|
263
|
+
}
|
|
264
|
+
print(options) {
|
|
265
|
+
options = defaults(options, {
|
|
266
|
+
ensureEmptyLineAbove: true
|
|
267
|
+
});
|
|
268
|
+
println(this.sprint(options));
|
|
269
|
+
}
|
|
270
|
+
constructor(/**
|
|
271
|
+
* The {@linkcode Context} that was used to initialize this
|
|
272
|
+
* {@linkcode SyncJson} instance.
|
|
273
|
+
*/ ctx, /**
|
|
274
|
+
* The root directory of the local filesystem, or in other words,
|
|
275
|
+
* the directory that contains the `.gadget/sync.json` file.
|
|
276
|
+
*/ directory, /**
|
|
277
|
+
* Indicates whether the environment was changed when this instance
|
|
278
|
+
* was loaded or initialized.
|
|
279
|
+
*/ previousEnvironment, /**
|
|
280
|
+
* The state of the `.gadget/sync.json` file on the local
|
|
281
|
+
* filesystem.
|
|
282
|
+
*/ state){
|
|
283
|
+
_define_property(this, "ctx", void 0);
|
|
284
|
+
_define_property(this, "directory", void 0);
|
|
285
|
+
_define_property(this, "previousEnvironment", void 0);
|
|
286
|
+
_define_property(this, "state", void 0);
|
|
287
|
+
/**
|
|
288
|
+
* The {@linkcode Application} that the directory is synced to.
|
|
289
|
+
*/ _define_property(this, "app", void 0);
|
|
290
|
+
/**
|
|
291
|
+
* The {@linkcode Environment} that the directory is synced to.
|
|
292
|
+
*/ _define_property(this, "env", void 0);
|
|
293
|
+
/**
|
|
294
|
+
* The last git branch that was checked out in the directory.
|
|
295
|
+
*/ _define_property(this, "gitBranch", void 0);
|
|
296
|
+
/**
|
|
297
|
+
* The {@linkcode Edit} client that can be used to send Gadget API
|
|
298
|
+
* requests to the environment that the directory is synced to.
|
|
299
|
+
*/ _define_property(this, "edit", void 0);
|
|
300
|
+
/**
|
|
301
|
+
* The last time the `.gadget/sync.json` file was modified.
|
|
302
|
+
*
|
|
303
|
+
* @deprecated
|
|
304
|
+
* We don't use this anymore, it's only here because older versions
|
|
305
|
+
* of ggt expect it to be in the .gadget/sync.json file.
|
|
306
|
+
*/ _define_property(this, "_mtime", void 0);
|
|
307
|
+
this.ctx = ctx;
|
|
308
|
+
this.directory = directory;
|
|
309
|
+
this.previousEnvironment = previousEnvironment;
|
|
310
|
+
this.state = state;
|
|
311
|
+
this.ctx = ctx.child({
|
|
312
|
+
fields: ()=>({
|
|
313
|
+
syncJson: {
|
|
314
|
+
directory: this.directory.path,
|
|
315
|
+
branch: this.gitBranch,
|
|
316
|
+
...this.state
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
assert(this.ctx.app, "app must be set on syncJson context");
|
|
321
|
+
this.app = this.ctx.app;
|
|
322
|
+
assert(this.ctx.env, "env must be set on syncJson context");
|
|
323
|
+
this.env = this.ctx.env;
|
|
324
|
+
this.edit = new Edit(this.ctx);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
export const loadSyncJsonDirectory = async (dir)=>{
|
|
328
|
+
if (config.windows && dir.startsWith("~/")) {
|
|
329
|
+
// "~" doesn't expand to the home directory on Windows
|
|
330
|
+
dir = homePath(dir.slice(2));
|
|
331
|
+
}
|
|
332
|
+
// TODO: rename to .gadget/ggt.json
|
|
333
|
+
const syncJsonPath = await findUp(".gadget/sync.json", {
|
|
334
|
+
cwd: dir
|
|
335
|
+
});
|
|
336
|
+
if (syncJsonPath) {
|
|
337
|
+
// we found a .gadget/sync.json file, use its parent directory
|
|
338
|
+
dir = path.join(syncJsonPath, "../..");
|
|
339
|
+
}
|
|
340
|
+
// ensure the directory path is absolute
|
|
341
|
+
dir = path.resolve(dir);
|
|
342
|
+
return await Directory.init(dir);
|
|
343
|
+
};
|
|
344
|
+
// ensure the selected app is valid
|
|
345
|
+
const loadApp = async (ctx, { availableApps, state })=>{
|
|
346
|
+
let appSlug = ctx.args["--app"] || state?.application;
|
|
347
|
+
if (!appSlug) {
|
|
348
|
+
// the user didn't specify an app, ask them to select one
|
|
349
|
+
appSlug = await select({
|
|
350
|
+
choices: availableApps.map((x)=>x.slug)
|
|
351
|
+
})`
|
|
352
|
+
Which application do you want to develop?
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
const app = availableApps.find((app)=>app.slug === appSlug);
|
|
356
|
+
if (app) {
|
|
357
|
+
// the user specified an app or we loaded it from the state,
|
|
358
|
+
// and it exists in their list of applications, so return it
|
|
359
|
+
return app;
|
|
360
|
+
}
|
|
361
|
+
// the specified appSlug doesn't exist in their list of apps,
|
|
362
|
+
// either they misspelled it or they don't have access to it
|
|
363
|
+
// anymore, suggest some apps that are similar to the one they
|
|
364
|
+
// specified
|
|
365
|
+
const similarAppSlugs = sortBySimilar(appSlug, availableApps.map((app)=>app.slug)).slice(0, 5);
|
|
366
|
+
// TODO: differentiate between incorrect --app vs state.application?
|
|
367
|
+
throw new ArgError(sprint`
|
|
368
|
+
Unknown application:
|
|
369
|
+
|
|
370
|
+
${appSlug}
|
|
371
|
+
|
|
372
|
+
Did you mean one of these?
|
|
373
|
+
|
|
374
|
+
• ${similarAppSlugs.join("\n • ")}
|
|
375
|
+
`);
|
|
376
|
+
};
|
|
377
|
+
const loadEnv = async (ctx, { app, state })=>{
|
|
378
|
+
if (ctx.args["--env"] && !app.multiEnvironmentEnabled) {
|
|
379
|
+
// this is a legacy app that only has 1 development environment, so
|
|
380
|
+
// let them know now rather than running into a weird error later
|
|
381
|
+
// TODO: come back to this
|
|
382
|
+
throw new ArgError(sprint`
|
|
383
|
+
You specified an environment but your app doesn't have multiple environments.
|
|
384
|
+
|
|
385
|
+
Remove the "--env" flag to sync to the {bold ${app.primaryDomain}} environment.
|
|
386
|
+
`);
|
|
387
|
+
}
|
|
388
|
+
const devEnvs = app.environments.filter((env)=>env.type === EnvironmentType.Development);
|
|
389
|
+
let envName = ctx.args["--env"] || state?.environment;
|
|
390
|
+
if (!envName) {
|
|
391
|
+
// user didn't specify an environment, ask them to select one
|
|
392
|
+
envName = await select({
|
|
393
|
+
choices: devEnvs.map((x)=>x.name)
|
|
394
|
+
})`
|
|
395
|
+
Which environment do you want to develop on?
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
if (envName.toLowerCase() === "production") {
|
|
399
|
+
// specifically call out that they can't dev, push, or pull to prod
|
|
400
|
+
throw new ArgError(sprint`
|
|
401
|
+
You cannot "ggt ${ctx.command}" your {bold production} environment.
|
|
402
|
+
`);
|
|
403
|
+
}
|
|
404
|
+
const env = devEnvs.find((env)=>env.name === envName.toLowerCase());
|
|
405
|
+
if (env) {
|
|
406
|
+
// the user specified an environment or we loaded it from the state,
|
|
407
|
+
// and it exists in the app's list of environments, so return it
|
|
408
|
+
return env;
|
|
409
|
+
}
|
|
410
|
+
// the specified env doesn't exist in their list of environments,
|
|
411
|
+
// either they misspelled it or they don't have access to it
|
|
412
|
+
// anymore, suggest some envs that are similar to the one they
|
|
413
|
+
// specified
|
|
414
|
+
const similarEnvironments = sortBySimilar(envName, devEnvs.map((env)=>env.name)).slice(0, 5);
|
|
415
|
+
throw new ArgError(sprint`
|
|
416
|
+
Unknown environment:
|
|
417
|
+
|
|
418
|
+
${envName}
|
|
419
|
+
|
|
420
|
+
Did you mean one of these?
|
|
421
|
+
|
|
422
|
+
• ${similarEnvironments.join("\n • ")}
|
|
423
|
+
`);
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* Returns the current git branch of the directory or undefined if
|
|
427
|
+
* the directory isn't a git repository.
|
|
428
|
+
*/ const loadBranch = async (ctx, { directory })=>{
|
|
429
|
+
try {
|
|
430
|
+
const branch = await simpleGit(directory.path).revparse([
|
|
431
|
+
"--abbrev-ref",
|
|
432
|
+
"HEAD"
|
|
433
|
+
]);
|
|
434
|
+
return branch;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
ctx.log.warn("failed to read git branch", {
|
|
437
|
+
error
|
|
438
|
+
});
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
export const SyncJsonStateV05 = z.object({
|
|
443
|
+
application: z.string(),
|
|
444
|
+
environment: z.string(),
|
|
445
|
+
environments: z.record(z.object({
|
|
446
|
+
filesVersion: z.string()
|
|
447
|
+
}))
|
|
448
|
+
});
|
|
449
|
+
export const SyncJsonStateV04 = z.object({
|
|
450
|
+
app: z.string(),
|
|
451
|
+
filesVersion: z.string(),
|
|
452
|
+
mtime: z.number()
|
|
453
|
+
});
|
|
454
|
+
export const AnySyncJsonState = z.union([
|
|
455
|
+
SyncJsonStateV05,
|
|
456
|
+
SyncJsonStateV04
|
|
457
|
+
]);
|
|
458
|
+
export const SyncJsonState = AnySyncJsonState.transform((state)=>{
|
|
459
|
+
if ("environment" in state) {
|
|
460
|
+
// it's a v0.5 state
|
|
461
|
+
return state;
|
|
462
|
+
}
|
|
463
|
+
// it's a v0.4 state, transform it to a v0.5 state
|
|
464
|
+
return {
|
|
465
|
+
application: state.app,
|
|
466
|
+
environment: "development",
|
|
467
|
+
environments: {
|
|
468
|
+
development: {
|
|
469
|
+
filesVersion: state.filesVersion
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
//# sourceMappingURL=sync-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/services/filesync/sync-json.ts"],"sourcesContent":["import { findUp } from \"find-up\";\nimport fs from \"fs-extra\";\nimport assert from \"node:assert\";\nimport path from \"node:path\";\nimport { simpleGit } from \"simple-git\";\nimport terminalLink from \"terminal-link\";\nimport { z } from \"zod\";\nimport { EnvironmentType, getApps, type Application, type Environment } from \"../app/app.js\";\nimport { AppArg } from \"../app/arg.js\";\nimport { Edit } from \"../app/edit/edit.js\";\nimport { ArgError, type ArgsDefinition } from \"../command/arg.js\";\nimport type { Context } from \"../command/context.js\";\nimport { config, homePath } from \"../config/config.js\";\nimport { println } from \"../output/print.js\";\nimport { select } from \"../output/select.js\";\nimport { sprint, sprintln, type SprintOptions } from \"../output/sprint.js\";\nimport { getUserOrLogin } from \"../user/user.js\";\nimport { sortBySimilar } from \"../util/collection.js\";\nimport { defaults } from \"../util/object.js\";\nimport { Directory } from \"./directory.js\";\nimport { UnknownDirectoryError } from \"./error.js\";\n\nexport const SyncJsonArgs = {\n \"--app\": { type: AppArg, alias: [\"-a\", \"--application\"] },\n \"--env\": { type: String, alias: [\"-e\", \"--environment\"] },\n \"--allow-unknown-directory\": Boolean,\n \"--allow-different-app\": Boolean,\n} satisfies ArgsDefinition;\n\nexport type SyncJsonArgs = typeof SyncJsonArgs;\n\n/**\n * The state of the filesystem.\n *\n * This is persisted to `.gadget/sync.json` within the {@linkcode directory}.\n */\n// TODO: rename and/or add to ctx?\nexport class SyncJson {\n /**\n * The {@linkcode Application} that the directory is synced to.\n */\n readonly app: Application;\n\n /**\n * The {@linkcode Environment} that the directory is synced to.\n */\n readonly env: Environment;\n\n /**\n * The last git branch that was checked out in the directory.\n */\n gitBranch: string | undefined;\n\n /**\n * The {@linkcode Edit} client that can be used to send Gadget API\n * requests to the environment that the directory is synced to.\n */\n readonly edit: Edit;\n\n /**\n * The last time the `.gadget/sync.json` file was modified.\n *\n * @deprecated\n * We don't use this anymore, it's only here because older versions\n * of ggt expect it to be in the .gadget/sync.json file.\n */\n private _mtime: number | undefined;\n\n private constructor(\n /**\n * The {@linkcode Context} that was used to initialize this\n * {@linkcode SyncJson} instance.\n */\n readonly ctx: Context<SyncJsonArgs>,\n\n /**\n * The root directory of the local filesystem, or in other words,\n * the directory that contains the `.gadget/sync.json` file.\n */\n readonly directory: Directory,\n\n /**\n * Indicates whether the environment was changed when this instance\n * was loaded or initialized.\n */\n readonly previousEnvironment: string | undefined,\n\n /**\n * The state of the `.gadget/sync.json` file on the local\n * filesystem.\n */\n readonly state: SyncJsonState,\n ) {\n this.ctx = ctx.child({\n fields: () => ({\n syncJson: {\n directory: this.directory.path,\n branch: this.gitBranch,\n ...this.state,\n },\n }),\n });\n\n assert(this.ctx.app, \"app must be set on syncJson context\");\n this.app = this.ctx.app;\n\n assert(this.ctx.env, \"env must be set on syncJson context\");\n this.env = this.ctx.env;\n\n this.edit = new Edit(this.ctx);\n }\n\n /**\n * The last filesVersion that was written to the filesystem.\n *\n * This determines if the filesystem in Gadget is ahead of the\n * filesystem on the local machine.\n */\n get filesVersion(): bigint {\n const environment = this.state.environments[this.state.environment];\n assert(environment, \"environment must exist in environments\");\n return BigInt(environment.filesVersion);\n }\n\n /**\n * Loads a {@linkcode SyncJson} from the specified directory.\n *\n * Returns undefined if the directory doesn't exist, is empty, or\n * doesn't contain a `.gadget/sync.json` file.\n */\n static async load(ctx: Context<SyncJsonArgs>, { directory }: { directory: Directory }): Promise<SyncJson | undefined> {\n ctx = ctx.child({ name: \"sync-json\" });\n\n const user = await getUserOrLogin(ctx);\n const availableApps = await getApps(ctx);\n if (availableApps.length === 0) {\n throw new ArgError(\n sprint`\n You (${user.email}) don't have have any Gadget applications.\n\n Visit https://gadget.new to create one!\n `,\n );\n }\n\n // try to load the .gadget/sync.json file\n const syncJsonFile = await fs\n .readFile(directory.absolute(\".gadget/sync.json\"), \"utf8\")\n .catch((error: unknown) => ctx.log.warn(\"failed to read .gadget/sync.json\", { error }));\n\n if (!syncJsonFile) {\n // the .gadget/sync.json file doesn't exist in the directory\n // or any of its parents\n return undefined;\n }\n\n // the .gadget/sync.json file exists, try to parse it\n let state: SyncJsonState | undefined;\n try {\n state = SyncJsonState.parse(JSON.parse(syncJsonFile));\n } catch (error) {\n // the .gadget/sync.json file exists, but it's invalid\n ctx.log.warn(\"failed to parse .gadget/sync.json\", { error, syncJsonFile });\n return undefined;\n }\n\n ctx.app = await loadApp(ctx, { availableApps, state });\n ctx.env = await loadEnv(ctx, { app: ctx.app, state });\n\n if (state.application !== ctx.app.slug) {\n // .gadget/sync.json is associated with a different app\n if (ctx.args[\"--allow-different-app\"]) {\n // the user passed --allow-different-app, so use the application\n // and environment they specified and clobber everything\n const syncJson = new SyncJson(ctx, directory, undefined, {\n application: ctx.app.slug,\n environment: ctx.env.name,\n environments: {\n [ctx.env.name]: { filesVersion: \"0\" },\n },\n });\n\n await syncJson.loadGitBranch();\n return syncJson;\n }\n\n // the user didn't pass --allow-different-app, so throw an error\n throw new ArgError(sprint`\n You were about to sync the following app to the following directory:\n\n ${ctx.app.slug} (${ctx.env.name}) → ${directory.path}\n\n However, that directory has already been synced with this app:\n\n ${state.application} (${state.environment})\n\n If you're sure that you want to sync:\n\n ${ctx.app.slug} (${ctx.env.name}) → ${directory.path}\n\n Run \"ggt dev\" with the {bold --allow-different-app} flag.\n `);\n }\n\n let previousEnvironment: string | undefined;\n if (state.environment !== ctx.env.name) {\n // the user specified a different environment, update the state\n println({ ensureEmptyLineAbove: true })`\n Changing environment.\n\n ${state.environment} → ${ctx.env.name}\n `;\n\n previousEnvironment = state.environment;\n state.environment = ctx.env.name;\n if (!state.environments[ctx.env.name]) {\n // the user has never synced to this environment before\n state.environments[ctx.env.name] = { filesVersion: \"0\" };\n }\n }\n\n const syncJson = new SyncJson(ctx, directory, previousEnvironment, state);\n await syncJson.save(syncJson.filesVersion);\n await syncJson.loadGitBranch();\n return syncJson;\n }\n\n /**\n * Loads a {@linkcode SyncJson} from the specified directory or\n * initializes a new one.\n *\n * - Ensures a user is logged in.\n * - Ensures the user has at least one application.\n * - Ensures the directory exists.\n * - Ensures the directory is empty or contains a `.gadget/sync.json` file, unless --allow-unknown-directory was passed\n * - Ensures the specified app matches the app the directory previously synced to, unless --allow-different-app was passed\n */\n // TODO: rename to loadOrAskAndInit\n static async loadOrInit(ctx: Context<SyncJsonArgs>, { directory }: { directory: Directory }): Promise<SyncJson> {\n ctx = ctx.child({ name: \"sync-json\" });\n\n let syncJson = await SyncJson.load(ctx, { directory });\n if (syncJson) {\n // the .gadget/sync.json file already exists and is valid\n return syncJson;\n }\n\n if ((await directory.hasFiles()) && !ctx.args[\"--allow-unknown-directory\"]) {\n // the directory isn't empty and the user didn't pass --allow-unknown-directory\n throw new UnknownDirectoryError(ctx, { directory });\n }\n\n ctx.app = await loadApp(ctx, { availableApps: await getApps(ctx) });\n ctx.env = await loadEnv(ctx, { app: ctx.app });\n\n // the directory is empty or the user passed\n // --allow-unknown-directory, either way ensure the directory exists\n // and create a fresh .gadget/sync.json file\n await fs.ensureDir(directory.path);\n\n syncJson = new SyncJson(ctx, directory, undefined, {\n application: ctx.app.slug,\n environment: ctx.env.name,\n environments: {\n [ctx.env.name]: { filesVersion: \"0\" },\n },\n });\n\n await syncJson.save(syncJson.filesVersion);\n await syncJson.loadGitBranch();\n\n return syncJson;\n }\n\n // TODO: just asks the user to select an app and environment, doesn't create a .gadget/sync.json file\n // static async loadOrAsk(ctx: Context<SyncJsonArgs>, { directory }: { directory: Directory }): Promise<SyncJson | undefined> {\n // }\n\n /**\n * Updates {@linkcode _syncJson} and saves it to `.gadget/sync.json`.\n */\n async save(filesVersion: string | bigint): Promise<void> {\n const environment = this.state.environments[this.state.environment];\n assert(environment, \"environment must exist in environments\");\n environment.filesVersion = String(filesVersion);\n\n this.ctx.log.debug(\"saving .gadget/sync.json\");\n this._mtime = Date.now();\n await fs.outputJSON(\n this.directory.absolute(\".gadget/sync.json\"),\n {\n ...this.state,\n\n // v0.4\n app: this.state.application,\n filesVersion: String(filesVersion),\n mtime: this._mtime,\n },\n { spaces: 2 },\n );\n }\n\n async loadGitBranch(): Promise<void> {\n this.gitBranch = await loadBranch(this.ctx, { directory: this.directory });\n }\n\n sprint(options: SprintOptions = {}): string {\n let str = sprintln`\n Application ${this.app.slug}\n Environment ${this.env.name}\n `;\n\n if (this.gitBranch) {\n str += sprintln({ indent: 5 })`Branch ${this.gitBranch}`;\n }\n\n if (terminalLink.isSupported) {\n str += sprintln({ ensureEmptyLineAbove: true })`\n ${terminalLink(\"Preview\", `https://${this.app.slug}--${this.env.name}.gadget.app`)} ${terminalLink(\"Editor\", `https://${this.app.primaryDomain}/edit/${this.env.name}`)} ${terminalLink(\"Playground\", `https://${this.app.primaryDomain}/api/playground/graphql?environment=${this.env.name}`)} ${terminalLink(\"Docs\", `https://docs.gadget.dev/api/${this.app.slug}`)}\n `;\n } else {\n str += sprintln`\n ------------------------\n Preview https://${this.app.slug}--${this.env.name}.gadget.app\n Editor https://${this.app.primaryDomain}/edit/${this.env.name}\n Playground https://${this.app.primaryDomain}/api/playground/graphql?environment=${this.env.name}\n Docs https://docs.gadget.dev/api/${this.app.slug}\n `;\n }\n\n return sprintln(options)(str);\n }\n\n print(options?: SprintOptions): void {\n options = defaults(options, { ensureEmptyLineAbove: true });\n println(this.sprint(options));\n }\n}\n\nexport const loadSyncJsonDirectory = async (dir: string): Promise<Directory> => {\n if (config.windows && dir.startsWith(\"~/\")) {\n // \"~\" doesn't expand to the home directory on Windows\n dir = homePath(dir.slice(2));\n }\n\n // TODO: rename to .gadget/ggt.json\n const syncJsonPath = await findUp(\".gadget/sync.json\", { cwd: dir });\n if (syncJsonPath) {\n // we found a .gadget/sync.json file, use its parent directory\n dir = path.join(syncJsonPath, \"../..\");\n }\n\n // ensure the directory path is absolute\n dir = path.resolve(dir);\n\n return await Directory.init(dir);\n};\n\n// ensure the selected app is valid\nconst loadApp = async (\n ctx: Context<SyncJsonArgs>,\n { availableApps, state }: { availableApps: Application[]; state?: SyncJsonState },\n): Promise<Application> => {\n let appSlug = ctx.args[\"--app\"] || state?.application;\n if (!appSlug) {\n // the user didn't specify an app, ask them to select one\n appSlug = await select({ choices: availableApps.map((x) => x.slug) })`\n Which application do you want to develop?\n `;\n }\n\n const app = availableApps.find((app) => app.slug === appSlug);\n if (app) {\n // the user specified an app or we loaded it from the state,\n // and it exists in their list of applications, so return it\n return app;\n }\n\n // the specified appSlug doesn't exist in their list of apps,\n // either they misspelled it or they don't have access to it\n // anymore, suggest some apps that are similar to the one they\n // specified\n const similarAppSlugs = sortBySimilar(\n appSlug,\n availableApps.map((app) => app.slug),\n ).slice(0, 5);\n\n // TODO: differentiate between incorrect --app vs state.application?\n throw new ArgError(\n sprint`\n Unknown application:\n\n ${appSlug}\n\n Did you mean one of these?\n\n • ${similarAppSlugs.join(\"\\n • \")}\n `,\n );\n};\n\nconst loadEnv = async (ctx: Context<SyncJsonArgs>, { app, state }: { app: Application; state?: SyncJsonState }): Promise<Environment> => {\n if (ctx.args[\"--env\"] && !app.multiEnvironmentEnabled) {\n // this is a legacy app that only has 1 development environment, so\n // let them know now rather than running into a weird error later\n // TODO: come back to this\n throw new ArgError(\n sprint`\n You specified an environment but your app doesn't have multiple environments.\n\n Remove the \"--env\" flag to sync to the {bold ${app.primaryDomain}} environment.\n `,\n );\n }\n\n const devEnvs = app.environments.filter((env) => env.type === EnvironmentType.Development);\n\n let envName = ctx.args[\"--env\"] || state?.environment;\n if (!envName) {\n // user didn't specify an environment, ask them to select one\n envName = await select({ choices: devEnvs.map((x) => x.name) })`\n Which environment do you want to develop on?\n `;\n }\n\n if (envName.toLowerCase() === \"production\") {\n // specifically call out that they can't dev, push, or pull to prod\n throw new ArgError(\n sprint`\n You cannot \"ggt ${ctx.command}\" your {bold production} environment.\n `,\n );\n }\n\n const env = devEnvs.find((env) => env.name === envName.toLowerCase());\n if (env) {\n // the user specified an environment or we loaded it from the state,\n // and it exists in the app's list of environments, so return it\n return env;\n }\n\n // the specified env doesn't exist in their list of environments,\n // either they misspelled it or they don't have access to it\n // anymore, suggest some envs that are similar to the one they\n // specified\n const similarEnvironments = sortBySimilar(\n envName,\n devEnvs.map((env) => env.name),\n ).slice(0, 5);\n\n throw new ArgError(\n sprint`\n Unknown environment:\n\n ${envName}\n\n Did you mean one of these?\n\n • ${similarEnvironments.join(\"\\n • \")}\n `,\n );\n};\n\n/**\n * Returns the current git branch of the directory or undefined if\n * the directory isn't a git repository.\n */\nconst loadBranch = async (ctx: Context<SyncJsonArgs>, { directory }: { directory: Directory }): Promise<string | undefined> => {\n try {\n const branch = await simpleGit(directory.path).revparse([\"--abbrev-ref\", \"HEAD\"]);\n return branch;\n } catch (error) {\n ctx.log.warn(\"failed to read git branch\", { error });\n return undefined;\n }\n};\n\nexport const SyncJsonStateV05 = z.object({\n application: z.string(),\n environment: z.string(),\n environments: z.record(z.object({ filesVersion: z.string() })),\n});\n\nexport const SyncJsonStateV04 = z.object({\n app: z.string(),\n filesVersion: z.string(),\n mtime: z.number(),\n});\n\nexport const AnySyncJsonState = z.union([SyncJsonStateV05, SyncJsonStateV04]);\n\nexport const SyncJsonState = AnySyncJsonState.transform((state): SyncJsonStateV05 => {\n if (\"environment\" in state) {\n // it's a v0.5 state\n return state;\n }\n\n // it's a v0.4 state, transform it to a v0.5 state\n return {\n application: state.app,\n environment: \"development\",\n environments: { development: { filesVersion: state.filesVersion } },\n };\n});\n\nexport type SyncJsonStateV05 = z.infer<typeof SyncJsonStateV05>;\nexport type SyncJsonStateV04 = z.infer<typeof SyncJsonStateV04>;\nexport type AnySyncJsonState = z.infer<typeof AnySyncJsonState>;\nexport type SyncJsonState = z.infer<typeof SyncJsonState>;\n"],"names":["findUp","fs","assert","path","simpleGit","terminalLink","z","EnvironmentType","getApps","AppArg","Edit","ArgError","config","homePath","println","select","sprint","sprintln","getUserOrLogin","sortBySimilar","defaults","Directory","UnknownDirectoryError","SyncJsonArgs","type","alias","String","Boolean","SyncJson","filesVersion","environment","state","environments","BigInt","load","ctx","directory","child","name","user","availableApps","length","email","syncJsonFile","readFile","absolute","catch","error","log","warn","undefined","SyncJsonState","parse","JSON","app","loadApp","env","loadEnv","application","slug","args","syncJson","loadGitBranch","previousEnvironment","ensureEmptyLineAbove","save","loadOrInit","hasFiles","ensureDir","debug","_mtime","Date","now","outputJSON","mtime","spaces","gitBranch","loadBranch","options","str","indent","isSupported","primaryDomain","print","edit","fields","branch","loadSyncJsonDirectory","dir","windows","startsWith","slice","syncJsonPath","cwd","join","resolve","init","appSlug","choices","map","x","find","similarAppSlugs","multiEnvironmentEnabled","devEnvs","filter","Development","envName","toLowerCase","command","similarEnvironments","revparse","SyncJsonStateV05","object","string","record","SyncJsonStateV04","number","AnySyncJsonState","union","transform","development"],"mappings":"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"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { HTTPError } from "got";
|
|
2
2
|
import { config } from "../config/config.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createLogger } from "../output/log/logger.js";
|
|
4
|
+
import { readSession, readToken } from "../user/session.js";
|
|
5
|
+
const log = createLogger({
|
|
6
|
+
name: "auth"
|
|
7
|
+
});
|
|
4
8
|
/**
|
|
5
9
|
* Determines whether the given request options are for a Gadget
|
|
6
10
|
* Services request.
|
|
@@ -19,6 +23,31 @@ import { readSession } from "../user/session.js";
|
|
|
19
23
|
const token = readSession();
|
|
20
24
|
return token && `session=${encodeURIComponent(token)};`;
|
|
21
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Loads the authentication headers.
|
|
28
|
+
*
|
|
29
|
+
* @returns The authentication headers as a record of key-value pairs, or undefined if no headers are available.
|
|
30
|
+
*/ export const loadAuthHeaders = ()=>{
|
|
31
|
+
const cookie = loadCookie();
|
|
32
|
+
if (cookie) {
|
|
33
|
+
log.trace("loading cookie as auth header", {
|
|
34
|
+
cookie
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
cookie
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const token = readToken();
|
|
41
|
+
if (token) {
|
|
42
|
+
log.trace("loading token as auth header", {
|
|
43
|
+
token
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
"x-platform-access-token": token
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
22
51
|
export const isUnauthorizedError = (error)=>{
|
|
23
52
|
return error instanceof HTTPError && error.response.statusCode === 401;
|
|
24
53
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/services/http/auth.ts"],"sourcesContent":["import { HTTPError, type OptionsInit } from \"got\";\nimport type { Context } from \"../command/context.js\";\nimport { config } from \"../config/config.js\";\nimport { readSession } from \"../user/session.js\";\n\n/**\n * Determines whether the given request options are for a Gadget\n * Services request.\n *\n * @param options - The request options to check.\n * @returns True if the request options are for a Gadget Services\n * request, false otherwise.\n */\nexport const isGadgetServicesRequest = (options: OptionsInit): boolean => {\n return options.url instanceof URL && options.url.host === config.domains.services;\n};\n\n/**\n * Loads the cookie from the session.\n *\n * @returns The cookie string or undefined if there is no session.\n */\nexport const loadCookie = (): string | undefined => {\n const token = readSession();\n return token && `session=${encodeURIComponent(token)};`;\n};\n\nexport const isUnauthorizedError = (error: unknown): error is HTTPError => {\n return error instanceof HTTPError && error.response.statusCode === 401;\n};\n\n/**\n * Swallows unauthorized errors and logs a warning, rethrows all other\n * errors.\n *\n * @param ctx - The current context.\n * @param error - The error to handle.\n */\nexport const swallowUnauthorized = (ctx: Context, error: unknown): void => {\n if (isUnauthorizedError(error)) {\n ctx.log.warn(\"swallowing unauthorized error\", { error });\n return;\n }\n throw error;\n};\n"],"names":["HTTPError","config","readSession","isGadgetServicesRequest","options","url","URL","host","domains","services","loadCookie","token","encodeURIComponent","isUnauthorizedError","error","response","statusCode","swallowUnauthorized","ctx","
|
|
1
|
+
{"version":3,"sources":["../../../src/services/http/auth.ts"],"sourcesContent":["import { HTTPError, type OptionsInit } from \"got\";\nimport type { Context } from \"../command/context.js\";\nimport { config } from \"../config/config.js\";\nimport { createLogger } from \"../output/log/logger.js\";\nimport { readSession, readToken } from \"../user/session.js\";\n\nconst log = createLogger({ name: \"auth\" });\n\n/**\n * Determines whether the given request options are for a Gadget\n * Services request.\n *\n * @param options - The request options to check.\n * @returns True if the request options are for a Gadget Services\n * request, false otherwise.\n */\nexport const isGadgetServicesRequest = (options: OptionsInit): boolean => {\n return options.url instanceof URL && options.url.host === config.domains.services;\n};\n\n/**\n * Loads the cookie from the session.\n *\n * @returns The cookie string or undefined if there is no session.\n */\nexport const loadCookie = (): string | undefined => {\n const token = readSession();\n return token && `session=${encodeURIComponent(token)};`;\n};\n\n/**\n * Loads the authentication headers.\n *\n * @returns The authentication headers as a record of key-value pairs, or undefined if no headers are available.\n */\nexport const loadAuthHeaders = (): Record<string, string> | undefined => {\n const cookie = loadCookie();\n if (cookie) {\n log.trace(\"loading cookie as auth header\", { cookie });\n return { cookie };\n }\n\n const token = readToken();\n\n if (token) {\n log.trace(\"loading token as auth header\", { token });\n return { \"x-platform-access-token\": token };\n }\n\n return undefined;\n};\n\nexport const isUnauthorizedError = (error: unknown): error is HTTPError => {\n return error instanceof HTTPError && error.response.statusCode === 401;\n};\n\n/**\n * Swallows unauthorized errors and logs a warning, rethrows all other\n * errors.\n *\n * @param ctx - The current context.\n * @param error - The error to handle.\n */\nexport const swallowUnauthorized = (ctx: Context, error: unknown): void => {\n if (isUnauthorizedError(error)) {\n ctx.log.warn(\"swallowing unauthorized error\", { error });\n return;\n }\n throw error;\n};\n"],"names":["HTTPError","config","createLogger","readSession","readToken","log","name","isGadgetServicesRequest","options","url","URL","host","domains","services","loadCookie","token","encodeURIComponent","loadAuthHeaders","cookie","trace","undefined","isUnauthorizedError","error","response","statusCode","swallowUnauthorized","ctx","warn"],"mappings":"AAAA,SAASA,SAAS,QAA0B,MAAM;AAElD,SAASC,MAAM,QAAQ,sBAAsB;AAC7C,SAASC,YAAY,QAAQ,0BAA0B;AACvD,SAASC,WAAW,EAAEC,SAAS,QAAQ,qBAAqB;AAE5D,MAAMC,MAAMH,aAAa;IAAEI,MAAM;AAAO;AAExC;;;;;;;CAOC,GACD,OAAO,MAAMC,0BAA0B,CAACC;IACtC,OAAOA,QAAQC,GAAG,YAAYC,OAAOF,QAAQC,GAAG,CAACE,IAAI,KAAKV,OAAOW,OAAO,CAACC,QAAQ;AACnF,EAAE;AAEF;;;;CAIC,GACD,OAAO,MAAMC,aAAa;IACxB,MAAMC,QAAQZ;IACd,OAAOY,SAAS,CAAC,QAAQ,EAAEC,mBAAmBD,OAAO,CAAC,CAAC;AACzD,EAAE;AAEF;;;;CAIC,GACD,OAAO,MAAME,kBAAkB;IAC7B,MAAMC,SAASJ;IACf,IAAII,QAAQ;QACVb,IAAIc,KAAK,CAAC,iCAAiC;YAAED;QAAO;QACpD,OAAO;YAAEA;QAAO;IAClB;IAEA,MAAMH,QAAQX;IAEd,IAAIW,OAAO;QACTV,IAAIc,KAAK,CAAC,gCAAgC;YAAEJ;QAAM;QAClD,OAAO;YAAE,2BAA2BA;QAAM;IAC5C;IAEA,OAAOK;AACT,EAAE;AAEF,OAAO,MAAMC,sBAAsB,CAACC;IAClC,OAAOA,iBAAiBtB,aAAasB,MAAMC,QAAQ,CAACC,UAAU,KAAK;AACrE,EAAE;AAEF;;;;;;CAMC,GACD,OAAO,MAAMC,sBAAsB,CAACC,KAAcJ;IAChD,IAAID,oBAAoBC,QAAQ;QAC9BI,IAAIrB,GAAG,CAACsB,IAAI,CAAC,iCAAiC;YAAEL;QAAM;QACtD;IACF;IACA,MAAMA;AACR,EAAE"}
|
|
@@ -97,6 +97,11 @@ const getContext = (options)=>{
|
|
|
97
97
|
request: error.request && {
|
|
98
98
|
method: error.request.options.method,
|
|
99
99
|
url: error.request.options.url?.toString()
|
|
100
|
+
},
|
|
101
|
+
response: error.response && {
|
|
102
|
+
statusCode: error.response.statusCode,
|
|
103
|
+
traceId: error.response.headers["x-trace-id"],
|
|
104
|
+
durationMs: error.response.timings.phases.total
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/services/http/http.ts"],"sourcesContent":["import { got, type OptionsInit } from \"got\";\nimport ms from \"ms\";\nimport assert from \"node:assert\";\nimport { Agent as HttpAgent } from \"node:http\";\nimport { Agent as HttpsAgent } from \"node:https\";\nimport { Context } from \"../command/context.js\";\nimport { config } from \"../config/config.js\";\nimport { sprint } from \"../output/sprint.js\";\nimport { writeSession } from \"../user/session.js\";\nimport { serializeError } from \"../util/object.js\";\nimport { isGadgetServicesRequest } from \"./auth.js\";\n\nexport type HttpOptions = OptionsInit;\n\nconst getContext = (options: HttpOptions): Context => {\n assert(\n options.context?.[\"ctx\"] instanceof Context,\n sprint(`\n ctx must be provided to http requests:\n\n const response = await http({\n context: { ctx },\n ...options,\n });\n `),\n );\n\n return options.context[\"ctx\"] as Context;\n};\n\n/**\n * An instance of the `got` library with hooks for logging and handling\n * 401 errors. This should be used for all HTTP requests.\n */\nexport const http = got.extend({\n agent: {\n http: new HttpAgent({ keepAlive: true }),\n https: new HttpsAgent({ keepAlive: true }),\n },\n retry: {\n limit: 10,\n methods: [\"GET\", \"PUT\", \"HEAD\", \"DELETE\", \"OPTIONS\", \"TRACE\"],\n statusCodes: [408, 413, 429, 500, 502, 503, 504, 521, 522, 524],\n errorCodes: [\n \"ETIMEDOUT\",\n \"ECONNRESET\",\n \"EADDRINUSE\",\n \"ECONNREFUSED\",\n \"EPIPE\",\n \"ENOTFOUND\",\n \"ENETUNREACH\",\n \"EAI_AGAIN\",\n \"EADDRNOTAVAIL\",\n \"EHOSTUNREACH\",\n ],\n maxRetryAfter: undefined,\n calculateDelay: ({ computedValue }) => computedValue,\n backoffLimit: ms(\"5s\"),\n noise: 100,\n },\n hooks: {\n beforeRequest: [\n (options) => {\n const ctx = getContext(options);\n options.signal = ctx.signal;\n options.headers[\"user-agent\"] = config.versionFull;\n ctx.log.debug(\"http request\", {\n http: {\n request: {\n method: options.method,\n url: options.url?.toString(),\n },\n },\n });\n },\n ],\n beforeRetry: [\n (error, retryCount) => {\n const ctx = getContext(error.request?.options ?? error.options.context);\n ctx.log.warn(\"http request failed, retrying...\", {\n http: {\n retryCount,\n error: serializeError(error),\n request: error.request && {\n method: error.request.options.method,\n url: error.request.options.url?.toString(),\n },\n },\n });\n },\n ],\n afterResponse: [\n (response) => {\n const ctx = getContext(response.request.options);\n ctx.log.debug(\"http response\", {\n http: {\n request: {\n method: response.request.options.method,\n url: response.request.options.url?.toString(),\n },\n response: {\n statusCode: response.statusCode,\n traceId: response.headers[\"x-trace-id\"],\n durationMs: response.timings.phases.total,\n },\n },\n });\n\n if (response.statusCode === 401 && isGadgetServicesRequest(response.request.options)) {\n // clear the session if the request was unauthorized\n writeSession(undefined);\n }\n\n return response;\n },\n ],\n },\n});\n"],"names":["got","ms","assert","Agent","HttpAgent","HttpsAgent","Context","config","sprint","writeSession","serializeError","isGadgetServicesRequest","getContext","options","context","http","extend","agent","keepAlive","https","retry","limit","methods","statusCodes","errorCodes","maxRetryAfter","undefined","calculateDelay","computedValue","backoffLimit","noise","hooks","beforeRequest","ctx","signal","headers","versionFull","log","debug","request","method","url","toString","beforeRetry","error","retryCount","warn","
|
|
1
|
+
{"version":3,"sources":["../../../src/services/http/http.ts"],"sourcesContent":["import { got, type OptionsInit } from \"got\";\nimport ms from \"ms\";\nimport assert from \"node:assert\";\nimport { Agent as HttpAgent } from \"node:http\";\nimport { Agent as HttpsAgent } from \"node:https\";\nimport { Context } from \"../command/context.js\";\nimport { config } from \"../config/config.js\";\nimport { sprint } from \"../output/sprint.js\";\nimport { writeSession } from \"../user/session.js\";\nimport { serializeError } from \"../util/object.js\";\nimport { isGadgetServicesRequest } from \"./auth.js\";\n\nexport type HttpOptions = OptionsInit;\n\nconst getContext = (options: HttpOptions): Context => {\n assert(\n options.context?.[\"ctx\"] instanceof Context,\n sprint(`\n ctx must be provided to http requests:\n\n const response = await http({\n context: { ctx },\n ...options,\n });\n `),\n );\n\n return options.context[\"ctx\"] as Context;\n};\n\n/**\n * An instance of the `got` library with hooks for logging and handling\n * 401 errors. This should be used for all HTTP requests.\n */\nexport const http = got.extend({\n agent: {\n http: new HttpAgent({ keepAlive: true }),\n https: new HttpsAgent({ keepAlive: true }),\n },\n retry: {\n limit: 10,\n methods: [\"GET\", \"PUT\", \"HEAD\", \"DELETE\", \"OPTIONS\", \"TRACE\"],\n statusCodes: [408, 413, 429, 500, 502, 503, 504, 521, 522, 524],\n errorCodes: [\n \"ETIMEDOUT\",\n \"ECONNRESET\",\n \"EADDRINUSE\",\n \"ECONNREFUSED\",\n \"EPIPE\",\n \"ENOTFOUND\",\n \"ENETUNREACH\",\n \"EAI_AGAIN\",\n \"EADDRNOTAVAIL\",\n \"EHOSTUNREACH\",\n ],\n maxRetryAfter: undefined,\n calculateDelay: ({ computedValue }) => computedValue,\n backoffLimit: ms(\"5s\"),\n noise: 100,\n },\n hooks: {\n beforeRequest: [\n (options) => {\n const ctx = getContext(options);\n options.signal = ctx.signal;\n options.headers[\"user-agent\"] = config.versionFull;\n ctx.log.debug(\"http request\", {\n http: {\n request: {\n method: options.method,\n url: options.url?.toString(),\n },\n },\n });\n },\n ],\n beforeRetry: [\n (error, retryCount) => {\n const ctx = getContext(error.request?.options ?? error.options.context);\n\n ctx.log.warn(\"http request failed, retrying...\", {\n http: {\n retryCount,\n error: serializeError(error),\n request: error.request && {\n method: error.request.options.method,\n url: error.request.options.url?.toString(),\n },\n response: error.response && {\n statusCode: error.response.statusCode,\n traceId: error.response.headers[\"x-trace-id\"],\n durationMs: error.response.timings.phases.total,\n },\n },\n });\n },\n ],\n afterResponse: [\n (response) => {\n const ctx = getContext(response.request.options);\n ctx.log.debug(\"http response\", {\n http: {\n request: {\n method: response.request.options.method,\n url: response.request.options.url?.toString(),\n },\n response: {\n statusCode: response.statusCode,\n traceId: response.headers[\"x-trace-id\"],\n durationMs: response.timings.phases.total,\n },\n },\n });\n\n if (response.statusCode === 401 && isGadgetServicesRequest(response.request.options)) {\n // clear the session if the request was unauthorized\n writeSession(undefined);\n }\n\n return response;\n },\n ],\n },\n});\n"],"names":["got","ms","assert","Agent","HttpAgent","HttpsAgent","Context","config","sprint","writeSession","serializeError","isGadgetServicesRequest","getContext","options","context","http","extend","agent","keepAlive","https","retry","limit","methods","statusCodes","errorCodes","maxRetryAfter","undefined","calculateDelay","computedValue","backoffLimit","noise","hooks","beforeRequest","ctx","signal","headers","versionFull","log","debug","request","method","url","toString","beforeRetry","error","retryCount","warn","response","statusCode","traceId","durationMs","timings","phases","total","afterResponse"],"mappings":"AAAA,SAASA,GAAG,QAA0B,MAAM;AAC5C,OAAOC,QAAQ,KAAK;AACpB,OAAOC,YAAY,cAAc;AACjC,SAASC,SAASC,SAAS,QAAQ,YAAY;AAC/C,SAASD,SAASE,UAAU,QAAQ,aAAa;AACjD,SAASC,OAAO,QAAQ,wBAAwB;AAChD,SAASC,MAAM,QAAQ,sBAAsB;AAC7C,SAASC,MAAM,QAAQ,sBAAsB;AAC7C,SAASC,YAAY,QAAQ,qBAAqB;AAClD,SAASC,cAAc,QAAQ,oBAAoB;AACnD,SAASC,uBAAuB,QAAQ,YAAY;AAIpD,MAAMC,aAAa,CAACC;IAClBX,OACEW,QAAQC,OAAO,EAAE,CAAC,MAAM,YAAYR,SACpCE,OAAO,CAAC;;;;;;;IAOR,CAAC;IAGH,OAAOK,QAAQC,OAAO,CAAC,MAAM;AAC/B;AAEA;;;CAGC,GACD,OAAO,MAAMC,OAAOf,IAAIgB,MAAM,CAAC;IAC7BC,OAAO;QACLF,MAAM,IAAIX,UAAU;YAAEc,WAAW;QAAK;QACtCC,OAAO,IAAId,WAAW;YAAEa,WAAW;QAAK;IAC1C;IACAE,OAAO;QACLC,OAAO;QACPC,SAAS;YAAC;YAAO;YAAO;YAAQ;YAAU;YAAW;SAAQ;QAC7DC,aAAa;YAAC;YAAK;YAAK;YAAK;YAAK;YAAK;YAAK;YAAK;YAAK;YAAK;SAAI;QAC/DC,YAAY;YACV;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;QACDC,eAAeC;QACfC,gBAAgB,CAAC,EAAEC,aAAa,EAAE,GAAKA;QACvCC,cAAc5B,GAAG;QACjB6B,OAAO;IACT;IACAC,OAAO;QACLC,eAAe;YACb,CAACnB;gBACC,MAAMoB,MAAMrB,WAAWC;gBACvBA,QAAQqB,MAAM,GAAGD,IAAIC,MAAM;gBAC3BrB,QAAQsB,OAAO,CAAC,aAAa,GAAG5B,OAAO6B,WAAW;gBAClDH,IAAII,GAAG,CAACC,KAAK,CAAC,gBAAgB;oBAC5BvB,MAAM;wBACJwB,SAAS;4BACPC,QAAQ3B,QAAQ2B,MAAM;4BACtBC,KAAK5B,QAAQ4B,GAAG,EAAEC;wBACpB;oBACF;gBACF;YACF;SACD;QACDC,aAAa;YACX,CAACC,OAAOC;gBACN,MAAMZ,MAAMrB,WAAWgC,MAAML,OAAO,EAAE1B,WAAW+B,MAAM/B,OAAO,CAACC,OAAO;gBAEtEmB,IAAII,GAAG,CAACS,IAAI,CAAC,oCAAoC;oBAC/C/B,MAAM;wBACJ8B;wBACAD,OAAOlC,eAAekC;wBACtBL,SAASK,MAAML,OAAO,IAAI;4BACxBC,QAAQI,MAAML,OAAO,CAAC1B,OAAO,CAAC2B,MAAM;4BACpCC,KAAKG,MAAML,OAAO,CAAC1B,OAAO,CAAC4B,GAAG,EAAEC;wBAClC;wBACAK,UAAUH,MAAMG,QAAQ,IAAI;4BAC1BC,YAAYJ,MAAMG,QAAQ,CAACC,UAAU;4BACrCC,SAASL,MAAMG,QAAQ,CAACZ,OAAO,CAAC,aAAa;4BAC7Ce,YAAYN,MAAMG,QAAQ,CAACI,OAAO,CAACC,MAAM,CAACC,KAAK;wBACjD;oBACF;gBACF;YACF;SACD;QACDC,eAAe;YACb,CAACP;gBACC,MAAMd,MAAMrB,WAAWmC,SAASR,OAAO,CAAC1B,OAAO;gBAC/CoB,IAAII,GAAG,CAACC,KAAK,CAAC,iBAAiB;oBAC7BvB,MAAM;wBACJwB,SAAS;4BACPC,QAAQO,SAASR,OAAO,CAAC1B,OAAO,CAAC2B,MAAM;4BACvCC,KAAKM,SAASR,OAAO,CAAC1B,OAAO,CAAC4B,GAAG,EAAEC;wBACrC;wBACAK,UAAU;4BACRC,YAAYD,SAASC,UAAU;4BAC/BC,SAASF,SAASZ,OAAO,CAAC,aAAa;4BACvCe,YAAYH,SAASI,OAAO,CAACC,MAAM,CAACC,KAAK;wBAC3C;oBACF;gBACF;gBAEA,IAAIN,SAASC,UAAU,KAAK,OAAOrC,wBAAwBoC,SAASR,OAAO,CAAC1B,OAAO,GAAG;oBACpF,oDAAoD;oBACpDJ,aAAaiB;gBACf;gBAEA,OAAOqB;YACT;SACD;IACH;AACF,GAAG"}
|