@gadgetinc/ggt 0.2.3 → 0.3.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.
Files changed (70) hide show
  1. package/README.md +79 -90
  2. package/bin/dev.js +11 -19
  3. package/bin/run.js +1 -9
  4. package/lib/__generated__/graphql.js +2 -2
  5. package/lib/__generated__/graphql.js.map +1 -1
  6. package/lib/commands/index.js +9 -0
  7. package/lib/commands/index.js.map +1 -0
  8. package/lib/commands/list.js +34 -49
  9. package/lib/commands/list.js.map +1 -1
  10. package/lib/commands/login.js +74 -17
  11. package/lib/commands/login.js.map +1 -1
  12. package/lib/commands/logout.js +19 -23
  13. package/lib/commands/logout.js.map +1 -1
  14. package/lib/commands/root.js +85 -0
  15. package/lib/commands/root.js.map +1 -0
  16. package/lib/commands/sync.js +313 -677
  17. package/lib/commands/sync.js.map +1 -1
  18. package/lib/commands/whoami.js +23 -27
  19. package/lib/commands/whoami.js.map +1 -1
  20. package/lib/main.js +12 -0
  21. package/lib/main.js.map +1 -0
  22. package/lib/services/app.js +36 -0
  23. package/lib/services/app.js.map +1 -0
  24. package/lib/services/args.js +28 -0
  25. package/lib/services/args.js.map +1 -0
  26. package/lib/services/client.js +43 -76
  27. package/lib/services/client.js.map +1 -1
  28. package/lib/services/config.js +139 -0
  29. package/lib/services/config.js.map +1 -0
  30. package/lib/services/errors.js +64 -89
  31. package/lib/services/errors.js.map +1 -1
  32. package/lib/services/filesync.js +404 -0
  33. package/lib/services/filesync.js.map +1 -0
  34. package/lib/services/fs-utils.js +18 -91
  35. package/lib/services/fs-utils.js.map +1 -1
  36. package/lib/services/http.js +53 -0
  37. package/lib/services/http.js.map +1 -0
  38. package/lib/services/log.js +45 -0
  39. package/lib/services/log.js.map +1 -0
  40. package/lib/services/notify.js +30 -0
  41. package/lib/services/notify.js.map +1 -0
  42. package/lib/services/output.js +59 -0
  43. package/lib/services/output.js.map +1 -0
  44. package/lib/services/promise.js +8 -5
  45. package/lib/services/promise.js.map +1 -1
  46. package/lib/services/session.js +27 -0
  47. package/lib/services/session.js.map +1 -0
  48. package/lib/services/sleep.js +2 -5
  49. package/lib/services/sleep.js.map +1 -1
  50. package/lib/services/timeout.js +8 -0
  51. package/lib/services/timeout.js.map +1 -0
  52. package/lib/services/user.js +70 -0
  53. package/lib/services/user.js.map +1 -0
  54. package/lib/services/version.js +72 -0
  55. package/lib/services/version.js.map +1 -0
  56. package/npm-shrinkwrap.json +16947 -26644
  57. package/package.json +68 -75
  58. package/lib/commands/help.js +0 -28
  59. package/lib/commands/help.js.map +0 -1
  60. package/lib/index.js +0 -3
  61. package/lib/index.js.map +0 -1
  62. package/lib/services/base-command.js +0 -203
  63. package/lib/services/base-command.js.map +0 -1
  64. package/lib/services/context.js +0 -143
  65. package/lib/services/context.js.map +0 -1
  66. package/lib/services/flags.js +0 -57
  67. package/lib/services/flags.js.map +0 -1
  68. package/lib/services/help.js +0 -36
  69. package/lib/services/help.js.map +0 -1
  70. package/oclif.manifest.json +0 -244
@@ -0,0 +1,404 @@
1
+ import { _ as _define_property } from "@swc/helpers/_/_define_property";
2
+ import chalkTemplate from "chalk-template";
3
+ import { findUp } from "find-up";
4
+ import fs from "fs-extra";
5
+ import ignore from "ignore";
6
+ import inquirer from "inquirer";
7
+ import _ from "lodash";
8
+ import ms from "ms";
9
+ import path from "node:path";
10
+ import process from "node:process";
11
+ import normalizePath from "normalize-path";
12
+ import pMap from "p-map";
13
+ import pRetry from "p-retry";
14
+ import pluralize from "pluralize";
15
+ import { dedent } from "ts-dedent";
16
+ import { z } from "zod";
17
+ import { getApps } from "./app.js";
18
+ import { config } from "./config.js";
19
+ import { ArgError, InvalidSyncFileError } from "./errors.js";
20
+ import { isEmptyDir, swallowEnoent } from "./fs-utils.js";
21
+ import { createLogger } from "./log.js";
22
+ import { println, sortByLevenshtein, sprint } from "./output.js";
23
+ const log = createLogger("filesync");
24
+ export class FileSync {
25
+ /**
26
+ * The last filesVersion that was written to the filesystem.
27
+ *
28
+ * This determines if the filesystem in Gadget is ahead of the
29
+ * filesystem on the local machine.
30
+ */ get filesVersion() {
31
+ return BigInt(this._state.filesVersion);
32
+ }
33
+ /**
34
+ * The largest mtime that was seen on the filesystem.
35
+ *
36
+ * This is used to determine if any files have changed since the last
37
+ * sync. This does not include the mtime of files that are ignored.
38
+ */ get mtime() {
39
+ return this._state.mtime;
40
+ }
41
+ /**
42
+ * Initializes a {@linkcode FileSync} instance.
43
+ * - Ensures the directory exists.
44
+ * - Ensures the directory is empty or contains a `.gadget/sync.json` file (unless `options.force` is `true`)
45
+ * - Ensures an app is specified (either via `options.app` or by prompting the user)
46
+ * - Ensures the specified app matches the app the directory was previously synced to (unless `options.force` is `true`)
47
+ */ static async init(user, options) {
48
+ const apps = await getApps(user);
49
+ if (apps.length === 0) {
50
+ throw new ArgError(sprint`
51
+ You (${user.email}) don't have have any Gadget applications.
52
+
53
+ Visit https://gadget.new to create one!
54
+ `);
55
+ }
56
+ let dir = options.dir;
57
+ if (!dir) {
58
+ // the user didn't specify a directory
59
+ const filepath = await findUp(".gadget/sync.json");
60
+ if (filepath) {
61
+ // we found a .gadget/sync.json file, use its parent directory
62
+ dir = path.join(filepath, "../..");
63
+ } else {
64
+ // we didn't find a .gadget/sync.json file, use the current directory
65
+ dir = process.cwd();
66
+ }
67
+ }
68
+ if (config.windows && _.startsWith(dir, "~/")) {
69
+ // `~` doesn't expand to the home directory on Windows
70
+ dir = path.join(config.homeDir, dir.slice(2));
71
+ }
72
+ // ensure the root directory is an absolute path and exists
73
+ await fs.ensureDir(dir = path.resolve(dir));
74
+ // try to load the .gadget/sync.json file
75
+ const state = await fs.readJson(path.join(dir, ".gadget/sync.json")).then(z.object({
76
+ app: z.string(),
77
+ filesVersion: z.string(),
78
+ mtime: z.number()
79
+ }).parse).catch(()=>undefined);
80
+ let appSlug = options.app || state?.app;
81
+ if (!appSlug) {
82
+ // the user didn't specify an app, suggest some apps that they can sync to
83
+ ({ appSlug } = await inquirer.prompt({
84
+ type: "list",
85
+ name: "appSlug",
86
+ message: "Please select the app to sync to.",
87
+ choices: _.map(apps, "slug")
88
+ }));
89
+ }
90
+ // try to find the appSlug in their list of apps
91
+ const app = _.find(apps, [
92
+ "slug",
93
+ appSlug
94
+ ]);
95
+ if (!app) {
96
+ // the specified appSlug doesn't exist in their list of apps,
97
+ // either they misspelled it or they don't have access to it
98
+ // anymore, suggest some apps that are similar to the one they
99
+ // specified
100
+ const similarAppSlugs = sortByLevenshtein(appSlug, _.map(apps, "slug")).slice(0, 5);
101
+ throw new ArgError(sprint`
102
+ Unknown application:
103
+
104
+ ${appSlug}
105
+
106
+ Did you mean one of these?
107
+
108
+
109
+ `.concat(` • ${similarAppSlugs.join("\n • ")}`));
110
+ }
111
+ const ignore = options.extraIgnorePaths ?? [];
112
+ if (!state) {
113
+ // the .gadget/sync.json file didn't exist or contained invalid json
114
+ if (await isEmptyDir(dir) || options.force) {
115
+ // the directory is empty or the user passed --force
116
+ // either way, create a fresh .gadget/sync.json file
117
+ return new FileSync(dir, app, ignore, {
118
+ app: app.slug,
119
+ filesVersion: "0",
120
+ mtime: 0
121
+ });
122
+ }
123
+ // the directory isn't empty and the user didn't pass --force
124
+ throw new InvalidSyncFileError(dir, app.slug);
125
+ }
126
+ // the .gadget/sync.json file exists
127
+ if (state.app === app.slug) {
128
+ // the .gadget/sync.json file is for the same app that the user specified
129
+ return new FileSync(dir, app, ignore, state);
130
+ }
131
+ // the .gadget/sync.json file is for a different app
132
+ if (options.force) {
133
+ // the user passed --force, so use the app they specified and overwrite everything
134
+ return new FileSync(dir, app, ignore, {
135
+ app: app.slug,
136
+ filesVersion: "0",
137
+ mtime: 0
138
+ });
139
+ }
140
+ // the user didn't pass --force, so throw an error
141
+ throw new ArgError(sprint`
142
+ You were about to sync the following app to the following directory:
143
+
144
+ {dim ${app.slug}} → {dim ${dir}}
145
+
146
+ However, that directory has already been synced with this app:
147
+
148
+ {dim ${state.app}}
149
+
150
+ If you're sure that you want to sync:
151
+
152
+ {dim ${app.slug}} → {dim ${dir}}
153
+
154
+ Then run {dim ggt sync} again with the {dim --force} flag.
155
+ `);
156
+ }
157
+ /**
158
+ * Converts an absolute path into a relative one from {@linkcode dir}.
159
+ */ relative(to) {
160
+ if (!path.isAbsolute(to)) {
161
+ // the filepath is already relative
162
+ return to;
163
+ }
164
+ return path.relative(this.dir, to);
165
+ }
166
+ /**
167
+ * Converts a relative path into an absolute one from {@linkcode dir}.
168
+ */ absolute(...pathSegments) {
169
+ return path.resolve(this.dir, ...pathSegments);
170
+ }
171
+ /**
172
+ * Similar to {@linkcode relative} in that it converts an absolute
173
+ * path into a relative one from {@linkcode dir}. However, it also
174
+ * changes any slashes to be posix/unix-like forward slashes,
175
+ * condenses repeated slashes into a single slash, and adds a trailing
176
+ * slash if the path is a directory.
177
+ *
178
+ * This is used when sending file-sync events to Gadget to ensure that
179
+ * the paths are consistent across platforms.
180
+ *
181
+ * @see https://www.npmjs.com/package/normalize-path
182
+ */ normalize(filepath, isDirectory) {
183
+ return normalizePath(path.isAbsolute(filepath) ? this.relative(filepath) : filepath) + (isDirectory ? "/" : "");
184
+ }
185
+ /**
186
+ * Reloads the ignore rules from the `.ignore` file.
187
+ */ reloadIgnorePaths() {
188
+ this._ignorer = ignore.default();
189
+ this._ignorer.add([
190
+ ".DS_Store",
191
+ "node_modules",
192
+ ".git",
193
+ ...this._extraIgnorePaths
194
+ ]);
195
+ try {
196
+ const content = fs.readFileSync(this.absolute(".ignore"), "utf-8");
197
+ this._ignorer.add(content);
198
+ log.info("reloaded ignore rules");
199
+ } catch (error) {
200
+ swallowEnoent(error);
201
+ }
202
+ }
203
+ /**
204
+ * Returns `true` if the {@linkcode filepath} should be ignored.
205
+ */ ignores(filepath) {
206
+ const relative = this.relative(filepath);
207
+ if (relative == "") {
208
+ // don't ignore the root dir
209
+ return false;
210
+ }
211
+ if (_.startsWith(relative, "..")) {
212
+ // anything above the root dir is ignored
213
+ return true;
214
+ }
215
+ return this._ignorer.ignores(relative);
216
+ }
217
+ /**
218
+ * Walks the directory and yields each file and directory.
219
+ *
220
+ * If a directory is empty, or only contains ignored entries, it will
221
+ * be yielded as a directory. Otherwise, each file within the
222
+ * directory will be yielded.
223
+ */ async *walkDir({ dir = this.dir, skipIgnored = true } = {}) {
224
+ // track whether the directory has any entries (ignored entries don't count)
225
+ let hasEntries = false;
226
+ for await (const entry of (await fs.opendir(dir))){
227
+ const filepath = path.join(dir, entry.name);
228
+ if (skipIgnored && this.ignores(filepath)) {
229
+ continue;
230
+ }
231
+ hasEntries = true;
232
+ if (entry.isDirectory()) {
233
+ yield* this.walkDir({
234
+ dir: filepath,
235
+ skipIgnored
236
+ });
237
+ } else if (entry.isFile()) {
238
+ yield [
239
+ filepath,
240
+ await fs.stat(filepath)
241
+ ];
242
+ }
243
+ }
244
+ if (!hasEntries) {
245
+ // if the directory is empty, or only contains ignored entries, yield it as a directory
246
+ yield [
247
+ `${dir}/`,
248
+ await fs.stat(dir)
249
+ ];
250
+ }
251
+ }
252
+ /**
253
+ * Writes the {@linkcode changed} and {@linkcode deleted} files to the filesystem.
254
+ * @param filesVersion The files version associated with the files that are being written.
255
+ * @param changed The files that have changed.
256
+ * @param deleted The paths that have been deleted.
257
+ * @param force If `true`, the files version will be updated even if it's less than the current files version.
258
+ */ async write(filesVersion, changed, deleted, force = false) {
259
+ filesVersion = BigInt(filesVersion);
260
+ await pMap(deleted, async (filepath)=>{
261
+ const currentPath = this.absolute(filepath);
262
+ const backupPath = this.absolute(".gadget/backup", this.relative(filepath));
263
+ // rather than `rm -rf`ing files, we move them to
264
+ // `.gadget/backup/` so that users can recover them if something
265
+ // goes wrong. We've seen a lot of EBUSY/EINVAL errors when moving
266
+ // files so we retry a few times.
267
+ await pRetry(async ()=>{
268
+ try {
269
+ // remove the current backup file in case it exists and is a
270
+ // different type (file vs directory)
271
+ await fs.remove(backupPath);
272
+ await fs.move(currentPath, backupPath);
273
+ } catch (error) {
274
+ // replicate the behavior of `rm -rf` and ignore ENOENT
275
+ swallowEnoent(error);
276
+ }
277
+ }, {
278
+ retries: 2,
279
+ minTimeout: ms("100ms"),
280
+ onFailedAttempt: (error)=>{
281
+ log.warn("failed to move file to backup", {
282
+ error
283
+ });
284
+ }
285
+ });
286
+ });
287
+ await pMap(changed, async (file)=>{
288
+ const absolutePath = this.absolute(file.path);
289
+ if (_.endsWith(file.path, "/")) {
290
+ await fs.ensureDir(absolutePath, {
291
+ mode: 0o755
292
+ });
293
+ return;
294
+ }
295
+ await fs.ensureDir(path.dirname(absolutePath), {
296
+ mode: 0o755
297
+ });
298
+ await fs.writeFile(absolutePath, Buffer.from(file.content, file.encoding), {
299
+ mode: file.mode
300
+ });
301
+ if (absolutePath == this.absolute(".ignore")) {
302
+ this.reloadIgnorePaths();
303
+ }
304
+ });
305
+ this._state.mtime = Date.now();
306
+ if (filesVersion > BigInt(this._state.filesVersion) || force) {
307
+ this._state.filesVersion = String(filesVersion);
308
+ }
309
+ this._save();
310
+ log.info("wrote", {
311
+ ...this._state,
312
+ changed: _.map(Array.from(changed), "path"),
313
+ deleted: Array.from(deleted)
314
+ });
315
+ }
316
+ /**
317
+ * Synchronously writes {@linkcode _state} to `.gadget/sync.json`.
318
+ */ _save() {
319
+ fs.outputJSONSync(this.absolute(".gadget/sync.json"), this._state, {
320
+ spaces: 2
321
+ });
322
+ }
323
+ constructor(/**
324
+ * An absolute path to the directory that is being synced.
325
+ */ dir, /**
326
+ * The Gadget application this filesystem is synced to.
327
+ */ app, /**
328
+ * Additional paths to ignore when syncing the filesystem.
329
+ */ _extraIgnorePaths, /**
330
+ * The state of the filesystem.
331
+ *
332
+ * This is persisted to `.gadget/sync.json`.
333
+ */ _state){
334
+ _define_property(this, "dir", void 0);
335
+ _define_property(this, "app", void 0);
336
+ _define_property(this, "_extraIgnorePaths", void 0);
337
+ _define_property(this, "_state", void 0);
338
+ /**
339
+ * The {@linkcode Ignore} instance that is used to determine if a file
340
+ * should be ignored.
341
+ *
342
+ * @see https://www.npmjs.com/package/ignore
343
+ */ _define_property(this, "_ignorer", void 0);
344
+ this.dir = dir;
345
+ this.app = app;
346
+ this._extraIgnorePaths = _extraIgnorePaths;
347
+ this._state = _state;
348
+ this._save();
349
+ this.reloadIgnorePaths();
350
+ }
351
+ }
352
+ /**
353
+ * Pretty-prints changed and deleted filepaths to the console.
354
+ *
355
+ * @param prefix The prefix to print before each line.
356
+ * @param changed The normalized paths that have changed.
357
+ * @param deleted The normalized paths that have been deleted.
358
+ * @param options.limit The maximum number of lines to print. Defaults to 10.
359
+ */ export const printPaths = (prefix, changed, deleted, { limit = 10 } = {})=>{
360
+ const lines = _.sortBy([
361
+ ..._.map(changed, (normalizedPath)=>chalkTemplate`{green ${prefix}} ${normalizedPath} {gray (changed)}`),
362
+ ..._.map(deleted, (normalizedPath)=>chalkTemplate`{red ${prefix}} ${normalizedPath} {gray (deleted)}`)
363
+ ], (line)=>line.slice(line.indexOf(" ") + 1));
364
+ let logged = 0;
365
+ for (const line of lines){
366
+ println(line);
367
+ if (++logged == limit) break;
368
+ }
369
+ if (lines.length > logged) {
370
+ println`{gray … ${lines.length - logged} more}`;
371
+ }
372
+ println`{gray ${pluralize("file", lines.length, true)} in total. ${changed.length} changed, ${deleted.length} deleted.}`;
373
+ println();
374
+ };
375
+ export const REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION = dedent(/* GraphQL */ `
376
+ subscription RemoteFileSyncEvents($localFilesVersion: String!) {
377
+ remoteFileSyncEvents(localFilesVersion: $localFilesVersion, encoding: base64) {
378
+ remoteFilesVersion
379
+ changed {
380
+ path
381
+ mode
382
+ content
383
+ encoding
384
+ }
385
+ deleted {
386
+ path
387
+ }
388
+ }
389
+ }
390
+ `);
391
+ export const REMOTE_FILES_VERSION_QUERY = dedent(/* GraphQL */ `
392
+ query RemoteFilesVersion {
393
+ remoteFilesVersion
394
+ }
395
+ `);
396
+ export const PUBLISH_FILE_SYNC_EVENTS_MUTATION = dedent(/* GraphQL */ `
397
+ mutation PublishFileSyncEvents($input: PublishFileSyncEventsInput!) {
398
+ publishFileSyncEvents(input: $input) {
399
+ remoteFilesVersion
400
+ }
401
+ }
402
+ `);
403
+
404
+ //# sourceMappingURL=filesync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/services/filesync.ts"],"sourcesContent":["import chalkTemplate from \"chalk-template\";\nimport { findUp } from \"find-up\";\nimport type { Stats } from \"fs-extra\";\nimport fs from \"fs-extra\";\nimport type { Ignore } from \"ignore\";\nimport ignore from \"ignore\";\nimport inquirer from \"inquirer\";\nimport _ from \"lodash\";\nimport ms from \"ms\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport normalizePath from \"normalize-path\";\nimport pMap from \"p-map\";\nimport pRetry from \"p-retry\";\nimport pluralize from \"pluralize\";\nimport { dedent } from \"ts-dedent\";\nimport { z } from \"zod\";\nimport type {\n PublishFileSyncEventsMutation,\n PublishFileSyncEventsMutationVariables,\n RemoteFileSyncEventsSubscription,\n RemoteFileSyncEventsSubscriptionVariables,\n RemoteFilesVersionQuery,\n RemoteFilesVersionQueryVariables,\n} from \"../__generated__/graphql.js\";\nimport type { App } from \"./app.js\";\nimport { getApps } from \"./app.js\";\nimport type { Query } from \"./client.js\";\nimport { config } from \"./config.js\";\nimport { ArgError, InvalidSyncFileError } from \"./errors.js\";\nimport { isEmptyDir, swallowEnoent } from \"./fs-utils.js\";\nimport { createLogger } from \"./log.js\";\nimport { println, sortByLevenshtein, sprint } from \"./output.js\";\nimport type { User } from \"./user.js\";\n\nconst log = createLogger(\"filesync\");\n\ninterface File {\n path: string;\n mode: number;\n content: string;\n encoding: \"utf8\" | \"base64\";\n}\n\nexport class FileSync {\n /**\n * The {@linkcode Ignore} instance that is used to determine if a file\n * should be ignored.\n *\n * @see https://www.npmjs.com/package/ignore\n */\n private _ignorer!: Ignore;\n\n private constructor(\n /**\n * An absolute path to the directory that is being synced.\n */\n readonly dir: string,\n\n /**\n * The Gadget application this filesystem is synced to.\n */\n readonly app: App,\n\n /**\n * Additional paths to ignore when syncing the filesystem.\n */\n private _extraIgnorePaths: string[],\n\n /**\n * The state of the filesystem.\n *\n * This is persisted to `.gadget/sync.json`.\n */\n private _state: { app: string; filesVersion: string; mtime: number },\n ) {\n this._save();\n this.reloadIgnorePaths();\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() {\n return BigInt(this._state.filesVersion);\n }\n\n /**\n * The largest mtime that was seen on the filesystem.\n *\n * This is used to determine if any files have changed since the last\n * sync. This does not include the mtime of files that are ignored.\n */\n get mtime() {\n return this._state.mtime;\n }\n\n /**\n * Initializes a {@linkcode FileSync} instance.\n * - Ensures the directory exists.\n * - Ensures the directory is empty or contains a `.gadget/sync.json` file (unless `options.force` is `true`)\n * - Ensures an app is specified (either via `options.app` or by prompting the user)\n * - Ensures the specified app matches the app the directory was previously synced to (unless `options.force` is `true`)\n */\n static async init(user: User, options: { dir?: string; app?: string; force?: boolean; extraIgnorePaths?: string[] }): Promise<FileSync> {\n const apps = await getApps(user);\n if (apps.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 let dir = options.dir;\n if (!dir) {\n // the user didn't specify a directory\n const filepath = await findUp(\".gadget/sync.json\");\n if (filepath) {\n // we found a .gadget/sync.json file, use its parent directory\n dir = path.join(filepath, \"../..\");\n } else {\n // we didn't find a .gadget/sync.json file, use the current directory\n dir = process.cwd();\n }\n }\n\n if (config.windows && _.startsWith(dir, \"~/\")) {\n // `~` doesn't expand to the home directory on Windows\n dir = path.join(config.homeDir, dir.slice(2));\n }\n\n // ensure the root directory is an absolute path and exists\n await fs.ensureDir((dir = path.resolve(dir)));\n\n // try to load the .gadget/sync.json file\n const state = await fs\n .readJson(path.join(dir, \".gadget/sync.json\"))\n .then(\n z.object({\n app: z.string(),\n filesVersion: z.string(),\n mtime: z.number(),\n }).parse,\n )\n .catch(() => undefined);\n\n let appSlug = options.app || state?.app;\n if (!appSlug) {\n // the user didn't specify an app, suggest some apps that they can sync to\n ({ appSlug } = await inquirer.prompt<{ appSlug: string }>({\n type: \"list\",\n name: \"appSlug\",\n message: \"Please select the app to sync to.\",\n choices: _.map(apps, \"slug\"),\n }));\n }\n\n // try to find the appSlug in their list of apps\n const app = _.find(apps, [\"slug\", appSlug]);\n if (!app) {\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 = sortByLevenshtein(appSlug, _.map(apps, \"slug\")).slice(0, 5);\n throw new ArgError(\n sprint`\n Unknown application:\n\n ${appSlug}\n\n Did you mean one of these?\n\n\n `.concat(` • ${similarAppSlugs.join(\"\\n • \")}`),\n );\n }\n\n const ignore = options.extraIgnorePaths ?? [];\n\n if (!state) {\n // the .gadget/sync.json file didn't exist or contained invalid json\n if ((await isEmptyDir(dir)) || options.force) {\n // the directory is empty or the user passed --force\n // either way, create a fresh .gadget/sync.json file\n return new FileSync(dir, app, ignore, { app: app.slug, filesVersion: \"0\", mtime: 0 });\n }\n\n // the directory isn't empty and the user didn't pass --force\n throw new InvalidSyncFileError(dir, app.slug);\n }\n\n // the .gadget/sync.json file exists\n if (state.app === app.slug) {\n // the .gadget/sync.json file is for the same app that the user specified\n return new FileSync(dir, app, ignore, state);\n }\n\n // the .gadget/sync.json file is for a different app\n if (options.force) {\n // the user passed --force, so use the app they specified and overwrite everything\n return new FileSync(dir, app, ignore, { app: app.slug, filesVersion: \"0\", mtime: 0 });\n }\n\n // the user didn't pass --force, so throw an error\n throw new ArgError(sprint`\n You were about to sync the following app to the following directory:\n\n {dim ${app.slug}} → {dim ${dir}}\n\n However, that directory has already been synced with this app:\n\n {dim ${state.app}}\n\n If you're sure that you want to sync:\n\n {dim ${app.slug}} → {dim ${dir}}\n\n Then run {dim ggt sync} again with the {dim --force} flag.\n `);\n }\n\n /**\n * Converts an absolute path into a relative one from {@linkcode dir}.\n */\n relative(to: string): string {\n if (!path.isAbsolute(to)) {\n // the filepath is already relative\n return to;\n }\n\n return path.relative(this.dir, to);\n }\n\n /**\n * Converts a relative path into an absolute one from {@linkcode dir}.\n */\n absolute(...pathSegments: string[]): string {\n return path.resolve(this.dir, ...pathSegments);\n }\n\n /**\n * Similar to {@linkcode relative} in that it converts an absolute\n * path into a relative one from {@linkcode dir}. However, it also\n * changes any slashes to be posix/unix-like forward slashes,\n * condenses repeated slashes into a single slash, and adds a trailing\n * slash if the path is a directory.\n *\n * This is used when sending file-sync events to Gadget to ensure that\n * the paths are consistent across platforms.\n *\n * @see https://www.npmjs.com/package/normalize-path\n */\n normalize(filepath: string, isDirectory: boolean): string {\n return normalizePath(path.isAbsolute(filepath) ? this.relative(filepath) : filepath) + (isDirectory ? \"/\" : \"\");\n }\n\n /**\n * Reloads the ignore rules from the `.ignore` file.\n */\n reloadIgnorePaths(): void {\n this._ignorer = ignore.default();\n this._ignorer.add([\".DS_Store\", \"node_modules\", \".git\", ...this._extraIgnorePaths]);\n\n try {\n const content = fs.readFileSync(this.absolute(\".ignore\"), \"utf-8\");\n this._ignorer.add(content);\n log.info(\"reloaded ignore rules\");\n } catch (error) {\n swallowEnoent(error);\n }\n }\n\n /**\n * Returns `true` if the {@linkcode filepath} should be ignored.\n */\n ignores(filepath: string): boolean {\n const relative = this.relative(filepath);\n if (relative == \"\") {\n // don't ignore the root dir\n return false;\n }\n\n if (_.startsWith(relative, \"..\")) {\n // anything above the root dir is ignored\n return true;\n }\n\n return this._ignorer.ignores(relative);\n }\n\n /**\n * Walks the directory and yields each file and directory.\n *\n * If a directory is empty, or only contains ignored entries, it will\n * be yielded as a directory. Otherwise, each file within the\n * directory will be yielded.\n */\n async *walkDir({ dir = this.dir, skipIgnored = true } = {}): AsyncGenerator<[absolutePath: string, entry: Stats]> {\n // track whether the directory has any entries (ignored entries don't count)\n let hasEntries = false;\n\n for await (const entry of await fs.opendir(dir)) {\n const filepath = path.join(dir, entry.name);\n if (skipIgnored && this.ignores(filepath)) {\n continue;\n }\n\n hasEntries = true;\n\n if (entry.isDirectory()) {\n yield* this.walkDir({ dir: filepath, skipIgnored });\n } else if (entry.isFile()) {\n yield [filepath, await fs.stat(filepath)];\n }\n }\n\n if (!hasEntries) {\n // if the directory is empty, or only contains ignored entries, yield it as a directory\n yield [`${dir}/`, await fs.stat(dir)];\n }\n }\n\n /**\n * Writes the {@linkcode changed} and {@linkcode deleted} files to the filesystem.\n * @param filesVersion The files version associated with the files that are being written.\n * @param changed The files that have changed.\n * @param deleted The paths that have been deleted.\n * @param force If `true`, the files version will be updated even if it's less than the current files version.\n */\n async write(filesVersion: bigint | string, changed: Iterable<File>, deleted: Iterable<string>, force = false): Promise<void> {\n filesVersion = BigInt(filesVersion);\n\n await pMap(deleted, async (filepath) => {\n const currentPath = this.absolute(filepath);\n const backupPath = this.absolute(\".gadget/backup\", this.relative(filepath));\n\n // rather than `rm -rf`ing files, we move them to\n // `.gadget/backup/` so that users can recover them if something\n // goes wrong. We've seen a lot of EBUSY/EINVAL errors when moving\n // files so we retry a few times.\n await pRetry(\n async () => {\n try {\n // remove the current backup file in case it exists and is a\n // different type (file vs directory)\n await fs.remove(backupPath);\n await fs.move(currentPath, backupPath);\n } catch (error) {\n // replicate the behavior of `rm -rf` and ignore ENOENT\n swallowEnoent(error);\n }\n },\n {\n retries: 2,\n minTimeout: ms(\"100ms\"),\n onFailedAttempt: (error) => {\n log.warn(\"failed to move file to backup\", { error });\n },\n },\n );\n });\n\n await pMap(changed, async (file) => {\n const absolutePath = this.absolute(file.path);\n if (_.endsWith(file.path, \"/\")) {\n await fs.ensureDir(absolutePath, { mode: 0o755 });\n return;\n }\n\n await fs.ensureDir(path.dirname(absolutePath), { mode: 0o755 });\n await fs.writeFile(absolutePath, Buffer.from(file.content, file.encoding), { mode: file.mode });\n\n if (absolutePath == this.absolute(\".ignore\")) {\n this.reloadIgnorePaths();\n }\n });\n\n this._state.mtime = Date.now();\n if (filesVersion > BigInt(this._state.filesVersion) || force) {\n this._state.filesVersion = String(filesVersion);\n }\n\n this._save();\n\n log.info(\"wrote\", {\n ...this._state,\n changed: _.map(Array.from(changed), \"path\"),\n deleted: Array.from(deleted),\n });\n }\n\n /**\n * Synchronously writes {@linkcode _state} to `.gadget/sync.json`.\n */\n private _save() {\n fs.outputJSONSync(this.absolute(\".gadget/sync.json\"), this._state, { spaces: 2 });\n }\n}\n\n/**\n * Pretty-prints changed and deleted filepaths to the console.\n *\n * @param prefix The prefix to print before each line.\n * @param changed The normalized paths that have changed.\n * @param deleted The normalized paths that have been deleted.\n * @param options.limit The maximum number of lines to print. Defaults to 10.\n */\nexport const printPaths = (prefix: string, changed: string[], deleted: string[], { limit = 10 } = {}) => {\n const lines = _.sortBy(\n [\n ..._.map(changed, (normalizedPath) => chalkTemplate`{green ${prefix}} ${normalizedPath} {gray (changed)}`),\n ..._.map(deleted, (normalizedPath) => chalkTemplate`{red ${prefix}} ${normalizedPath} {gray (deleted)}`),\n ],\n (line) => line.slice(line.indexOf(\" \") + 1),\n );\n\n let logged = 0;\n for (const line of lines) {\n println(line);\n if (++logged == limit) break;\n }\n\n if (lines.length > logged) {\n println`{gray … ${lines.length - logged} more}`;\n }\n\n println`{gray ${pluralize(\"file\", lines.length, true)} in total. ${changed.length} changed, ${deleted.length} deleted.}`;\n println();\n};\n\nexport const REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION = dedent(/* GraphQL */ `\n subscription RemoteFileSyncEvents($localFilesVersion: String!) {\n remoteFileSyncEvents(localFilesVersion: $localFilesVersion, encoding: base64) {\n remoteFilesVersion\n changed {\n path\n mode\n content\n encoding\n }\n deleted {\n path\n }\n }\n }\n`) as Query<RemoteFileSyncEventsSubscription, RemoteFileSyncEventsSubscriptionVariables>;\n\nexport const REMOTE_FILES_VERSION_QUERY = dedent(/* GraphQL */ `\n query RemoteFilesVersion {\n remoteFilesVersion\n }\n`) as Query<RemoteFilesVersionQuery, RemoteFilesVersionQueryVariables>;\n\nexport const PUBLISH_FILE_SYNC_EVENTS_MUTATION = dedent(/* GraphQL */ `\n mutation PublishFileSyncEvents($input: PublishFileSyncEventsInput!) {\n publishFileSyncEvents(input: $input) {\n remoteFilesVersion\n }\n }\n`) as Query<PublishFileSyncEventsMutation, PublishFileSyncEventsMutationVariables>;\n"],"names":["chalkTemplate","findUp","fs","ignore","inquirer","_","ms","path","process","normalizePath","pMap","pRetry","pluralize","dedent","z","getApps","config","ArgError","InvalidSyncFileError","isEmptyDir","swallowEnoent","createLogger","println","sortByLevenshtein","sprint","log","FileSync","filesVersion","BigInt","_state","mtime","init","user","options","apps","length","email","dir","filepath","join","cwd","windows","startsWith","homeDir","slice","ensureDir","resolve","state","readJson","then","object","app","string","number","parse","catch","undefined","appSlug","prompt","type","name","message","choices","map","find","similarAppSlugs","concat","extraIgnorePaths","force","slug","relative","to","isAbsolute","absolute","pathSegments","normalize","isDirectory","reloadIgnorePaths","_ignorer","default","add","_extraIgnorePaths","content","readFileSync","info","error","ignores","walkDir","skipIgnored","hasEntries","entry","opendir","isFile","stat","write","changed","deleted","currentPath","backupPath","remove","move","retries","minTimeout","onFailedAttempt","warn","file","absolutePath","endsWith","mode","dirname","writeFile","Buffer","from","encoding","Date","now","String","_save","Array","outputJSONSync","spaces","printPaths","prefix","limit","lines","sortBy","normalizedPath","line","indexOf","logged","REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION","REMOTE_FILES_VERSION_QUERY","PUBLISH_FILE_SYNC_EVENTS_MUTATION"],"mappings":"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"}
@@ -1,106 +1,33 @@
1
- import { _ as _define_property } from "@swc/helpers/_/_define_property";
2
- import Debug from "debug";
3
1
  import fs from "fs-extra";
4
- import ignore from "ignore";
5
- import path from "path";
6
- const debug = Debug("ggt:fs-utils");
7
- export class FSIgnorer {
8
- ignores(filepath) {
9
- const relative = path.isAbsolute(filepath) ? path.relative(this._rootDir, filepath) : filepath;
10
- if (relative == "") return false;
11
- // anything above the root dir is ignored
12
- if (relative == "..") {
13
- return true;
14
- }
15
- return this._ignorer.ignores(relative);
16
- }
17
- reload() {
18
- this._ignorer = ignore.default();
19
- this._ignorer.add(this._alwaysIgnore);
20
- try {
21
- this._ignorer.add(fs.readFileSync(this.filepath, "utf-8"));
22
- debug("reloaded ignore rules from %s", path.relative(this._rootDir, this.filepath));
23
- } catch (error) {
24
- ignoreEnoent(error);
25
- }
26
- }
27
- constructor(_rootDir, _alwaysIgnore){
28
- _define_property(this, "_rootDir", void 0);
29
- _define_property(this, "_alwaysIgnore", void 0);
30
- _define_property(this, "filepath", void 0);
31
- _define_property(this, "_ignorer", void 0);
32
- this._rootDir = _rootDir;
33
- this._alwaysIgnore = _alwaysIgnore;
34
- this.filepath = path.join(this._rootDir, ".ignore");
35
- this.reload();
36
- }
37
- }
38
- export async function* walkDir(dir, options = {}) {
39
- if (options.ignorer?.ignores(dir)) return;
40
- if (await isEmptyDir(dir)) {
41
- yield `${dir}/`;
42
- return;
43
- }
44
- for await (const entry of (await fs.opendir(dir))){
45
- const filepath = path.join(dir, entry.name);
46
- if (entry.isDirectory()) {
47
- yield* walkDir(filepath, options);
48
- } else if (entry.isFile() && !options.ignorer?.ignores(filepath)) {
49
- yield filepath;
50
- }
51
- }
52
- }
53
- export function* walkDirSync(dir, options = {}) {
54
- if (options.ignorer?.ignores(dir)) return;
55
- if (isEmptyDirSync(dir)) {
56
- yield `${dir}/`;
2
+ import path from "node:path";
3
+ import { createLogger } from "./log.js";
4
+ const log = createLogger("fs");
5
+ export function swallowEnoent(error) {
6
+ if (error.code === "ENOENT") {
7
+ log.debug("swallowing enoent error", {
8
+ path: path.basename(error.path)
9
+ });
57
10
  return;
58
11
  }
59
- for (const entry of fs.readdirSync(dir, {
60
- withFileTypes: true
61
- })){
62
- const filepath = path.join(dir, entry.name);
63
- if (entry.isDirectory()) {
64
- yield* walkDirSync(filepath, options);
65
- } else if (entry.isFile() && !options.ignorer?.ignores(filepath)) {
66
- yield filepath;
67
- }
68
- }
69
- }
70
- export function isEmptyDirSync(dir, opts = {
71
- ignoreEnoent: true
72
- }) {
73
- try {
74
- const files = fs.readdirSync(dir);
75
- return files.length === 0;
76
- } catch (error) {
77
- if (opts.ignoreEnoent) {
78
- ignoreEnoent(error);
79
- return true;
80
- }
81
- throw error;
82
- }
12
+ throw error;
83
13
  }
84
14
  export async function isEmptyDir(dir, opts = {
85
- ignoreEnoent: true
15
+ swallowEnoent: true
86
16
  }) {
87
17
  try {
88
- const files = await fs.readdir(dir);
89
- return files.length === 0;
18
+ for await (const _ of (await fs.opendir(dir, {
19
+ bufferSize: 1
20
+ }))){
21
+ return false;
22
+ }
23
+ return true;
90
24
  } catch (error) {
91
- if (opts.ignoreEnoent) {
92
- ignoreEnoent(error);
25
+ if (opts.swallowEnoent) {
26
+ swallowEnoent(error);
93
27
  return true;
94
28
  }
95
29
  throw error;
96
30
  }
97
31
  }
98
- export function ignoreEnoent(error) {
99
- if (error.code === "ENOENT") {
100
- debug("ignoring ENOENT error %s", path.basename(error.path));
101
- return;
102
- }
103
- throw error;
104
- }
105
32
 
106
33
  //# sourceMappingURL=fs-utils.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/fs-utils.ts"],"sourcesContent":["import Debug from \"debug\";\nimport fs from \"fs-extra\";\nimport type { Ignore } from \"ignore\";\nimport ignore from \"ignore\";\nimport path from \"path\";\n\nconst debug = Debug(\"ggt:fs-utils\");\n\nexport class FSIgnorer {\n readonly filepath;\n\n private _ignorer!: Ignore;\n\n constructor(\n private readonly _rootDir: string,\n private readonly _alwaysIgnore: string[],\n ) {\n this.filepath = path.join(this._rootDir, \".ignore\");\n this.reload();\n }\n\n ignores(filepath: string): boolean {\n const relative = path.isAbsolute(filepath) ? path.relative(this._rootDir, filepath) : filepath;\n if (relative == \"\") return false;\n // anything above the root dir is ignored\n if (relative == \"..\") {\n return true;\n }\n return this._ignorer.ignores(relative);\n }\n\n reload(): void {\n this._ignorer = ignore.default();\n this._ignorer.add(this._alwaysIgnore);\n\n try {\n this._ignorer.add(fs.readFileSync(this.filepath, \"utf-8\"));\n debug(\"reloaded ignore rules from %s\", path.relative(this._rootDir, this.filepath));\n } catch (error) {\n ignoreEnoent(error);\n }\n }\n}\n\nexport interface WalkDirOptions {\n ignorer?: FSIgnorer;\n}\n\nexport async function* walkDir(dir: string, options: WalkDirOptions = {}): AsyncGenerator<string> {\n if (options.ignorer?.ignores(dir)) return;\n\n if (await isEmptyDir(dir)) {\n yield `${dir}/`;\n return;\n }\n\n for await (const entry of await fs.opendir(dir)) {\n const filepath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n yield* walkDir(filepath, options);\n } else if (entry.isFile() && !options.ignorer?.ignores(filepath)) {\n yield filepath;\n }\n }\n}\n\nexport function* walkDirSync(dir: string, options: WalkDirOptions = {}): Generator<string> {\n if (options.ignorer?.ignores(dir)) return;\n\n if (isEmptyDirSync(dir)) {\n yield `${dir}/`;\n return;\n }\n\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const filepath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n yield* walkDirSync(filepath, options);\n } else if (entry.isFile() && !options.ignorer?.ignores(filepath)) {\n yield filepath;\n }\n }\n}\n\nexport function isEmptyDirSync(dir: string, opts = { ignoreEnoent: true }): boolean {\n try {\n const files = fs.readdirSync(dir);\n return files.length === 0;\n } catch (error) {\n if (opts.ignoreEnoent) {\n ignoreEnoent(error);\n return true;\n }\n throw error;\n }\n}\n\nexport async function isEmptyDir(dir: string, opts = { ignoreEnoent: true }): Promise<boolean> {\n try {\n const files = await fs.readdir(dir);\n return files.length === 0;\n } catch (error) {\n if (opts.ignoreEnoent) {\n ignoreEnoent(error);\n return true;\n }\n throw error;\n }\n}\n\nexport function ignoreEnoent(error: any): void {\n if (error.code === \"ENOENT\") {\n debug(\"ignoring ENOENT error %s\", path.basename(error.path as string));\n return;\n }\n throw error;\n}\n"],"names":["Debug","fs","ignore","path","debug","FSIgnorer","ignores","filepath","relative","isAbsolute","_rootDir","_ignorer","reload","default","add","_alwaysIgnore","readFileSync","error","ignoreEnoent","constructor","join","walkDir","dir","options","ignorer","isEmptyDir","entry","opendir","name","isDirectory","isFile","walkDirSync","isEmptyDirSync","readdirSync","withFileTypes","opts","files","length","readdir","code","basename"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/services/fs-utils.ts"],"sourcesContent":["import fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { createLogger } from \"./log.js\";\n\nconst log = createLogger(\"fs\");\n\nexport function swallowEnoent(error: any): void {\n if (error.code === \"ENOENT\") {\n log.debug(\"swallowing enoent error\", { path: path.basename(error.path as string) });\n return;\n }\n throw error;\n}\n\nexport async function isEmptyDir(dir: string, opts = { swallowEnoent: true }): Promise<boolean> {\n try {\n for await (const _ of await fs.opendir(dir, { bufferSize: 1 })) {\n return false;\n }\n return true;\n } catch (error) {\n if (opts.swallowEnoent) {\n swallowEnoent(error);\n return true;\n }\n throw error;\n }\n}\n"],"names":["fs","path","createLogger","log","swallowEnoent","error","code","debug","basename","isEmptyDir","dir","opts","_","opendir","bufferSize"],"mappings":"AAAA,OAAOA,QAAQ,WAAW;AAC1B,OAAOC,UAAU,YAAY;AAC7B,SAASC,YAAY,QAAQ,WAAW;AAExC,MAAMC,MAAMD,aAAa;AAEzB,OAAO,SAASE,cAAcC,KAAU;IACtC,IAAIA,MAAMC,IAAI,KAAK,UAAU;QAC3BH,IAAII,KAAK,CAAC,2BAA2B;YAAEN,MAAMA,KAAKO,QAAQ,CAACH,MAAMJ,IAAI;QAAY;QACjF;IACF;IACA,MAAMI;AACR;AAEA,OAAO,eAAeI,WAAWC,GAAW,EAAEC,OAAO;IAAEP,eAAe;AAAK,CAAC;IAC1E,IAAI;QACF,WAAW,MAAMQ,KAAK,CAAA,MAAMZ,GAAGa,OAAO,CAACH,KAAK;YAAEI,YAAY;QAAE,EAAC,EAAG;YAC9D,OAAO;QACT;QACA,OAAO;IACT,EAAE,OAAOT,OAAO;QACd,IAAIM,KAAKP,aAAa,EAAE;YACtBA,cAAcC;YACd,OAAO;QACT;QACA,MAAMA;IACR;AACF"}
@@ -0,0 +1,53 @@
1
+ import { got, HTTPError } from "got";
2
+ import { config } from "./config.js";
3
+ import { createLogger } from "./log.js";
4
+ import { readSession, writeSession } from "./session.js";
5
+ const log = createLogger("http");
6
+ export const http = got.extend({
7
+ hooks: {
8
+ beforeRequest: [
9
+ (options)=>{
10
+ options.headers["user-agent"] = config.versionFull;
11
+ log.info("http request", {
12
+ method: options.method,
13
+ url: options.url?.toString()
14
+ });
15
+ }
16
+ ],
17
+ afterResponse: [
18
+ (response)=>{
19
+ log.info("http response", {
20
+ method: response.request.options.method,
21
+ url: response.request.options.url?.toString(),
22
+ statusCode: response.statusCode,
23
+ traceId: response.headers["x-trace-id"]
24
+ });
25
+ if (response.statusCode === 401 && isGadgetRequest(response.request.options)) {
26
+ writeSession(undefined);
27
+ }
28
+ return response;
29
+ }
30
+ ]
31
+ }
32
+ });
33
+ export const isUnauthorized = (error)=>{
34
+ return error instanceof HTTPError && error.response.statusCode === 401 && isGadgetRequest(error.request.options);
35
+ };
36
+ export const swallowUnauthorized = (error)=>{
37
+ if (isUnauthorized(error)) {
38
+ log.warn("swallowing unauthorized error", {
39
+ error
40
+ });
41
+ return;
42
+ }
43
+ throw error;
44
+ };
45
+ export const loadCookie = ()=>{
46
+ const token = readSession();
47
+ return token && `session=${encodeURIComponent(token)};`;
48
+ };
49
+ const isGadgetRequest = (options)=>{
50
+ return options.url instanceof URL && options.url.host === config.domains.services;
51
+ };
52
+
53
+ //# sourceMappingURL=http.js.map