neon 2.47.0 → 3.1.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 +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +395 -0
- package/dist/_shared/env-core/reuse-secrets.js +179 -0
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +575 -658
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +642 -681
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -341
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -64
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +133 -147
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +9 -7
package/dist/init/bootstrap.js
CHANGED
|
@@ -1,448 +1,377 @@
|
|
|
1
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync
|
|
1
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { gunzipSync } from "fflate";
|
|
4
3
|
import YAML from "yaml";
|
|
4
|
+
import { gunzipSync } from "fflate";
|
|
5
|
+
//#region src/init/bootstrap.ts
|
|
5
6
|
/** Default features when a template doesn't specify `requires`. */
|
|
6
7
|
const DEFAULT_REQUIRES = ["database"];
|
|
7
8
|
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
9
|
+
* Hardcoded fallback used when every remote manifest source is unreachable.
|
|
10
|
+
* Kept in sync with `neondatabase/examples/bootstrap.yaml` (the source of
|
|
11
|
+
* truth) so that, even fully offline from the manifest, the picker still offers
|
|
12
|
+
* the full set of starters rather than a single template.
|
|
13
|
+
*/
|
|
14
|
+
const FALLBACK_TEMPLATES = [
|
|
15
|
+
{
|
|
16
|
+
id: "hono",
|
|
17
|
+
title: "REST API",
|
|
18
|
+
description: "A Hono REST API on Neon Functions, backed by Lakebase Postgres via Drizzle.",
|
|
19
|
+
tools: ["Hono", "Drizzle"],
|
|
20
|
+
services: ["Postgres", "Functions"],
|
|
21
|
+
requires: ["database", "functions"],
|
|
22
|
+
source: {
|
|
23
|
+
owner: "neondatabase",
|
|
24
|
+
repo: "examples",
|
|
25
|
+
ref: "main",
|
|
26
|
+
subdir: "with-hono"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "ai-sdk",
|
|
31
|
+
title: "Image-generation agent",
|
|
32
|
+
description: "A Vercel AI SDK agent that streams chat through the Neon AI Gateway and stores generated images in Neon object storage, indexed in Postgres via Drizzle.",
|
|
33
|
+
tools: ["AI SDK", "Drizzle"],
|
|
34
|
+
services: [
|
|
35
|
+
"Postgres",
|
|
36
|
+
"Functions",
|
|
37
|
+
"Object Storage",
|
|
38
|
+
"AI Gateway"
|
|
39
|
+
],
|
|
40
|
+
requires: [
|
|
41
|
+
"database",
|
|
42
|
+
"functions",
|
|
43
|
+
"object-storage",
|
|
44
|
+
"ai-gateway"
|
|
45
|
+
],
|
|
46
|
+
source: {
|
|
47
|
+
owner: "neondatabase",
|
|
48
|
+
repo: "examples",
|
|
49
|
+
ref: "main",
|
|
50
|
+
subdir: "with-ai-sdk"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "mastra",
|
|
55
|
+
title: "Personal-assistant agent",
|
|
56
|
+
description: "A Mastra agent that streams chat through the Neon AI Gateway and uses Mastra Memory on Lakebase Postgres to remember you across threads.",
|
|
57
|
+
tools: ["Mastra", "Mastra Memory"],
|
|
58
|
+
services: [
|
|
59
|
+
"Postgres",
|
|
60
|
+
"Functions",
|
|
61
|
+
"AI Gateway"
|
|
62
|
+
],
|
|
63
|
+
requires: [
|
|
64
|
+
"database",
|
|
65
|
+
"functions",
|
|
66
|
+
"ai-gateway"
|
|
67
|
+
],
|
|
68
|
+
source: {
|
|
69
|
+
owner: "neondatabase",
|
|
70
|
+
repo: "examples",
|
|
71
|
+
ref: "main",
|
|
72
|
+
subdir: "with-mastra"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
56
75
|
];
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
const templateIds = (templates) => templates.map((t) => t.id).join(", ");
|
|
77
|
+
const findTemplate = (templates, id) => templates.find((t) => t.id === id);
|
|
59
78
|
const githubToken = () => process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? "";
|
|
60
|
-
// A token is never required for public templates, but we forward it when
|
|
61
|
-
// present so the same code path works behind proxies that authenticate, and
|
|
62
|
-
// (in future) for private template repos.
|
|
63
79
|
const downloadHeaders = () => ({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"User-Agent": "neon",
|
|
67
|
-
...(githubToken() ? { Authorization: `Bearer ${githubToken()}` } : {}),
|
|
80
|
+
"User-Agent": "neon",
|
|
81
|
+
...githubToken() ? { Authorization: `Bearer ${githubToken()}` } : {}
|
|
68
82
|
});
|
|
69
|
-
// The codeload host is overridable so the e2e tests can point the downloader at
|
|
70
|
-
// a local server (the same trick `--api-host` uses to redirect the Neon API).
|
|
71
83
|
const codeloadBase = () => process.env.NEON_BOOTSTRAP_GITHUB_CODELOAD ?? "https://codeload.github.com";
|
|
72
84
|
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
73
85
|
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
* Normalize a manifest entry's string list (`tools` or `services`) into a clean
|
|
87
|
+
* array. Tolerant by design: a missing or non-array value yields `undefined`,
|
|
88
|
+
* and non-string/blank items are dropped, so a malformed list never sinks an
|
|
89
|
+
* otherwise-valid template (it just renders without that detail).
|
|
90
|
+
*/
|
|
79
91
|
const parseStringList = (value) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return items.length > 0 ? items : undefined;
|
|
92
|
+
if (!Array.isArray(value)) return void 0;
|
|
93
|
+
const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
94
|
+
return items.length > 0 ? items : void 0;
|
|
84
95
|
};
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Remote template manifest
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Primary manifest host is neon.com (CDN-backed, no GitHub rate limiting), with
|
|
89
|
-
// the raw GitHub copy as a fallback and the hardcoded list as the last resort.
|
|
90
|
-
// A single env override (used by tests) short-circuits the chain.
|
|
91
96
|
const NEON_MANIFEST_URL = "https://neon.com/bootstrap/templates.yaml";
|
|
92
97
|
const GITHUB_RAW_MANIFEST_URL = "https://raw.githubusercontent.com/neondatabase/examples/main/bootstrap.yaml";
|
|
93
98
|
function manifestUrls() {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return [NEON_MANIFEST_URL, GITHUB_RAW_MANIFEST_URL];
|
|
99
|
+
const override = process.env.NEON_BOOTSTRAP_MANIFEST_URL;
|
|
100
|
+
if (override) return [override];
|
|
101
|
+
return [NEON_MANIFEST_URL, GITHUB_RAW_MANIFEST_URL];
|
|
98
102
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
templates.push({
|
|
125
|
-
id: item.id,
|
|
126
|
-
title: item.title,
|
|
127
|
-
description: item.description,
|
|
128
|
-
...(tools ? { tools } : {}),
|
|
129
|
-
...(services ? { services } : {}),
|
|
130
|
-
requires,
|
|
131
|
-
source: {
|
|
132
|
-
owner: item.source.owner,
|
|
133
|
-
repo: item.source.repo,
|
|
134
|
-
ref: item.source.ref,
|
|
135
|
-
subdir: item.source.subdir,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
return templates;
|
|
103
|
+
function parseManifest(text) {
|
|
104
|
+
const data = YAML.parse(text);
|
|
105
|
+
if (!isRecord(data) || !Array.isArray(data.templates)) throw new Error("Invalid bootstrap manifest: missing \"templates\" array.");
|
|
106
|
+
const templates = [];
|
|
107
|
+
for (const item of data.templates) {
|
|
108
|
+
if (!isRecord(item) || typeof item.id !== "string" || typeof item.title !== "string" || typeof item.description !== "string" || !isRecord(item.source) || typeof item.source.owner !== "string" || typeof item.source.repo !== "string" || typeof item.source.ref !== "string" || typeof item.source.subdir !== "string") continue;
|
|
109
|
+
const requires = Array.isArray(item.requires) && item.requires.every((r) => typeof r === "string") ? item.requires : DEFAULT_REQUIRES;
|
|
110
|
+
const tools = parseStringList(item.tools);
|
|
111
|
+
const services = parseStringList(item.services);
|
|
112
|
+
templates.push({
|
|
113
|
+
id: item.id,
|
|
114
|
+
title: item.title,
|
|
115
|
+
description: item.description,
|
|
116
|
+
...tools ? { tools } : {},
|
|
117
|
+
...services ? { services } : {},
|
|
118
|
+
requires,
|
|
119
|
+
source: {
|
|
120
|
+
owner: item.source.owner,
|
|
121
|
+
repo: item.source.repo,
|
|
122
|
+
ref: item.source.ref,
|
|
123
|
+
subdir: item.source.subdir
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return templates;
|
|
140
128
|
}
|
|
141
129
|
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return templates;
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
// Try the next source.
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return FALLBACK_TEMPLATES;
|
|
130
|
+
* Fetch the template manifest, trying each source in {@link manifestUrls} in
|
|
131
|
+
* order and returning the first that yields a non-empty template list. Falls
|
|
132
|
+
* back to the hardcoded list when every source is unreachable or empty, so the
|
|
133
|
+
* picker never fails just because a host is down.
|
|
134
|
+
*/
|
|
135
|
+
async function fetchTemplates() {
|
|
136
|
+
for (const url of manifestUrls()) try {
|
|
137
|
+
const res = await fetch(url, {
|
|
138
|
+
headers: downloadHeaders(),
|
|
139
|
+
signal: AbortSignal.timeout(1e4)
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
142
|
+
const templates = parseManifest(await res.text());
|
|
143
|
+
if (templates.length > 0) return templates;
|
|
144
|
+
} catch {}
|
|
145
|
+
return FALLBACK_TEMPLATES;
|
|
165
146
|
}
|
|
166
147
|
const TAR_BLOCK = 512;
|
|
167
148
|
const readTarString = (buf, offset, length) => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return buf.toString("utf8", offset, end);
|
|
149
|
+
let end = offset;
|
|
150
|
+
const max = offset + length;
|
|
151
|
+
while (end < max && buf[end] !== 0) end++;
|
|
152
|
+
return buf.toString("utf8", offset, end);
|
|
173
153
|
};
|
|
174
154
|
const readTarOctal = (buf, offset, length) => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return Number.isNaN(value) ? 0 : value;
|
|
155
|
+
const text = readTarString(buf, offset, length).trim();
|
|
156
|
+
if (text === "") return 0;
|
|
157
|
+
const value = parseInt(text, 8);
|
|
158
|
+
return Number.isNaN(value) ? 0 : value;
|
|
180
159
|
};
|
|
181
160
|
const isZeroBlock = (buf, offset) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
return true;
|
|
161
|
+
for (let i = offset; i < offset + TAR_BLOCK; i++) if (buf[i] !== 0) return false;
|
|
162
|
+
return true;
|
|
187
163
|
};
|
|
188
164
|
/**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
165
|
+
* Parse pax extended-header records ("<len> <key>=<value>\n"). GitHub uses
|
|
166
|
+
* these for the global header and for any path that doesn't fit the legacy
|
|
167
|
+
* 100-byte name field, so we must honor at least `path` and `linkpath`.
|
|
168
|
+
*/
|
|
193
169
|
const parsePaxRecords = (data) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
pos += len;
|
|
209
|
-
}
|
|
210
|
-
return records;
|
|
170
|
+
const records = {};
|
|
171
|
+
let pos = 0;
|
|
172
|
+
const text = data.toString("utf8");
|
|
173
|
+
while (pos < text.length) {
|
|
174
|
+
const space = text.indexOf(" ", pos);
|
|
175
|
+
if (space === -1) break;
|
|
176
|
+
const len = parseInt(text.slice(pos, space), 10);
|
|
177
|
+
if (Number.isNaN(len) || len <= 0) break;
|
|
178
|
+
const record = text.slice(space + 1, pos + len - 1);
|
|
179
|
+
const eq = record.indexOf("=");
|
|
180
|
+
if (eq !== -1) records[record.slice(0, eq)] = record.slice(eq + 1);
|
|
181
|
+
pos += len;
|
|
182
|
+
}
|
|
183
|
+
return records;
|
|
211
184
|
};
|
|
212
185
|
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (overrideLink !== undefined)
|
|
266
|
-
linkname = overrideLink;
|
|
267
|
-
overridePath = undefined;
|
|
268
|
-
overrideLink = undefined;
|
|
269
|
-
entries.push({ name, type, mode, linkname, data: Buffer.from(data) });
|
|
270
|
-
}
|
|
271
|
-
return entries;
|
|
186
|
+
* Decode a (decompressed) tar archive into its file/symlink entries. Pure and
|
|
187
|
+
* dependency-free so it can be unit tested without touching the network.
|
|
188
|
+
* Handles the ustar `prefix` field, pax extended headers (type 'x'/'g'), and
|
|
189
|
+
* GNU long-name/long-link headers (type 'L'/'K') so deep template paths and
|
|
190
|
+
* long symlink targets round-trip correctly.
|
|
191
|
+
*/
|
|
192
|
+
const parseTar = (buf) => {
|
|
193
|
+
const entries = [];
|
|
194
|
+
let overridePath;
|
|
195
|
+
let overrideLink;
|
|
196
|
+
let offset = 0;
|
|
197
|
+
while (offset + TAR_BLOCK <= buf.length) {
|
|
198
|
+
if (isZeroBlock(buf, offset)) break;
|
|
199
|
+
let name = readTarString(buf, offset, 100);
|
|
200
|
+
const mode = readTarOctal(buf, offset + 100, 8);
|
|
201
|
+
const size = readTarOctal(buf, offset + 124, 12);
|
|
202
|
+
const typeByte = buf[offset + 156];
|
|
203
|
+
const type = typeByte === 0 ? "0" : String.fromCharCode(typeByte);
|
|
204
|
+
let linkname = readTarString(buf, offset + 157, 100);
|
|
205
|
+
if (readTarString(buf, offset + 257, 6).startsWith("ustar")) {
|
|
206
|
+
const prefix = readTarString(buf, offset + 345, 155);
|
|
207
|
+
if (prefix !== "") name = `${prefix}/${name}`;
|
|
208
|
+
}
|
|
209
|
+
offset += TAR_BLOCK;
|
|
210
|
+
const data = buf.subarray(offset, offset + size);
|
|
211
|
+
offset += Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
|
|
212
|
+
if (type === "x") {
|
|
213
|
+
const records = parsePaxRecords(data);
|
|
214
|
+
if (records.path !== void 0) overridePath = records.path;
|
|
215
|
+
if (records.linkpath !== void 0) overrideLink = records.linkpath;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (type === "g") continue;
|
|
219
|
+
if (type === "L" || type === "K") {
|
|
220
|
+
const longValue = data.toString("utf8").replace(/\0+$/, "");
|
|
221
|
+
if (type === "L") overridePath = longValue;
|
|
222
|
+
else overrideLink = longValue;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (overridePath !== void 0) name = overridePath;
|
|
226
|
+
if (overrideLink !== void 0) linkname = overrideLink;
|
|
227
|
+
overridePath = void 0;
|
|
228
|
+
overrideLink = void 0;
|
|
229
|
+
entries.push({
|
|
230
|
+
name,
|
|
231
|
+
type,
|
|
232
|
+
mode,
|
|
233
|
+
linkname,
|
|
234
|
+
data: Buffer.from(data)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return entries;
|
|
272
238
|
};
|
|
273
239
|
/**
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
executable: (entry.mode & 0o111) !== 0,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
// Directories ('5') and any other node types are intentionally skipped.
|
|
306
|
-
}
|
|
307
|
-
return files;
|
|
240
|
+
* Map decoded tar entries to the files under `subdir`, with the top-level
|
|
241
|
+
* archive directory and the `subdir/` prefix stripped from each path. Pure so
|
|
242
|
+
* it can be unit tested. Directory and other non-regular entries are dropped —
|
|
243
|
+
* writing files re-creates their parent directories.
|
|
244
|
+
*/
|
|
245
|
+
const selectTemplateFiles = (entries, subdir) => {
|
|
246
|
+
const prefix = `${subdir.replace(/^\/+|\/+$/g, "")}/`;
|
|
247
|
+
const files = [];
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
const slash = entry.name.indexOf("/");
|
|
250
|
+
if (slash === -1) continue;
|
|
251
|
+
const repoPath = entry.name.slice(slash + 1);
|
|
252
|
+
if (!repoPath.startsWith(prefix)) continue;
|
|
253
|
+
const path = repoPath.slice(prefix.length);
|
|
254
|
+
if (path === "") continue;
|
|
255
|
+
if (entry.type === "2") files.push({
|
|
256
|
+
kind: "symlink",
|
|
257
|
+
path,
|
|
258
|
+
target: entry.linkname
|
|
259
|
+
});
|
|
260
|
+
else if (entry.type === "0" || entry.type === "7") files.push({
|
|
261
|
+
kind: "file",
|
|
262
|
+
path,
|
|
263
|
+
bytes: entry.data,
|
|
264
|
+
executable: (entry.mode & 73) !== 0
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return files;
|
|
308
268
|
};
|
|
309
269
|
const tarballUrl = (template) => {
|
|
310
|
-
|
|
311
|
-
|
|
270
|
+
const { owner, repo, ref } = template.source;
|
|
271
|
+
return `${codeloadBase()}/${owner}/${repo}/tar.gz/${ref}`;
|
|
312
272
|
};
|
|
313
273
|
const friendlyGithubError = (status, url) => {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (status === 403 || status === 429) {
|
|
318
|
-
return new Error(`GitHub rate limited the template download (${url}). Set a GITHUB_TOKEN environment variable to raise the limit, then retry.`);
|
|
319
|
-
}
|
|
320
|
-
return new Error(`GitHub returned HTTP ${status} for ${url}.`);
|
|
274
|
+
if (status === 404) return /* @__PURE__ */ new Error(`GitHub returned 404 for ${url}. The template repo or ref may have moved.`);
|
|
275
|
+
if (status === 403 || status === 429) return /* @__PURE__ */ new Error(`GitHub rate limited the template download (${url}). Set a GITHUB_TOKEN environment variable to raise the limit, then retry.`);
|
|
276
|
+
return /* @__PURE__ */ new Error(`GitHub returned HTTP ${status} for ${url}.`);
|
|
321
277
|
};
|
|
322
278
|
/**
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const files = selectTemplateFiles(parseTar(tar), subdir);
|
|
352
|
-
if (files.length === 0) {
|
|
353
|
-
throw new Error(`Template subdirectory "${subdir}" was not found in ${owner}/${repo}@${ref}.`);
|
|
354
|
-
}
|
|
355
|
-
return files;
|
|
279
|
+
* Download a template and resolve it to the exact set of files to write. The
|
|
280
|
+
* entire subtree is captured in one tarball request, so the copy is atomically
|
|
281
|
+
* consistent: a push to the template repo mid-download cannot produce a
|
|
282
|
+
* mismatched checkout (unlike fetching a file list and then each blob).
|
|
283
|
+
*/
|
|
284
|
+
const downloadTemplate = async (template) => {
|
|
285
|
+
const url = tarballUrl(template);
|
|
286
|
+
let gzipped;
|
|
287
|
+
try {
|
|
288
|
+
const res = await fetch(url, {
|
|
289
|
+
headers: downloadHeaders(),
|
|
290
|
+
signal: AbortSignal.timeout(3e4)
|
|
291
|
+
});
|
|
292
|
+
if (!res.ok) throw friendlyGithubError(res.status, url);
|
|
293
|
+
gzipped = Buffer.from(await res.arrayBuffer());
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
296
|
+
}
|
|
297
|
+
let tar;
|
|
298
|
+
try {
|
|
299
|
+
tar = Buffer.from(gunzipSync(new Uint8Array(gzipped)));
|
|
300
|
+
} catch (err) {
|
|
301
|
+
throw new Error(`Failed to decompress the template archive from ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
302
|
+
}
|
|
303
|
+
const { owner, repo, ref, subdir } = template.source;
|
|
304
|
+
const files = selectTemplateFiles(parseTar(tar), subdir);
|
|
305
|
+
if (files.length === 0) throw new Error(`Template subdirectory "${subdir}" was not found in ${owner}/${repo}@${ref}.`);
|
|
306
|
+
return files;
|
|
356
307
|
};
|
|
357
|
-
// ---------------------------------------------------------------------------
|
|
358
|
-
// Target validation + scaffolding to disk
|
|
359
|
-
// ---------------------------------------------------------------------------
|
|
360
308
|
/**
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
309
|
+
* A bad caller-supplied input that an agent (or human) can correct: an unknown
|
|
310
|
+
* template id or a non-empty target directory. Carries an `agentCode` so an
|
|
311
|
+
* agent surface can report a precise error code instead of a generic
|
|
312
|
+
* INTERNAL_ERROR, while a human path just surfaces the clear `message`.
|
|
313
|
+
*/
|
|
314
|
+
var BootstrapInputError = class extends Error {
|
|
315
|
+
constructor(message, agentCode) {
|
|
316
|
+
super(message);
|
|
317
|
+
this.name = "BootstrapInputError";
|
|
318
|
+
this.agentCode = agentCode;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
373
321
|
/**
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
throw new BootstrapInputError(`Target ${dir} already exists and is not a directory.`, "TARGET_NOT_DIRECTORY");
|
|
384
|
-
}
|
|
385
|
-
const contents = readdirSync(dir).filter((name) => name !== ".git");
|
|
386
|
-
if (contents.length > 0 && !force) {
|
|
387
|
-
throw new BootstrapInputError(`Target directory ${dir} is not empty. Use --force to scaffold into it anyway (colliding files will be overwritten), or choose an empty directory.`, "TARGET_NOT_EMPTY");
|
|
388
|
-
}
|
|
322
|
+
* Ensure `dir` is safe to scaffold into: it must be missing, or an empty
|
|
323
|
+
* directory (a lone `.git` is ignored so you can scaffold into a freshly
|
|
324
|
+
* `git init`ed folder). `force` allows scaffolding into a non-empty directory,
|
|
325
|
+
* overwriting colliding files. Throws a {@link BootstrapInputError} otherwise.
|
|
326
|
+
*/
|
|
327
|
+
const ensureTargetUsable = (dir, force) => {
|
|
328
|
+
if (!existsSync(dir)) return;
|
|
329
|
+
if (!statSync(dir).isDirectory()) throw new BootstrapInputError(`Target ${dir} already exists and is not a directory.`, "TARGET_NOT_DIRECTORY");
|
|
330
|
+
if (readdirSync(dir).filter((name) => name !== ".git").length > 0 && !force) throw new BootstrapInputError(`Target directory ${dir} is not empty. Use --force to scaffold into it anyway (colliding files will be overwritten), or choose an empty directory.`, "TARGET_NOT_EMPTY");
|
|
389
331
|
};
|
|
390
332
|
const isSymlink = (path) => {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
333
|
+
try {
|
|
334
|
+
return lstatSync(path).isSymbolicLink();
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
397
338
|
};
|
|
398
339
|
const errnoCode = (err) => {
|
|
399
|
-
|
|
400
|
-
err !== null &&
|
|
401
|
-
"code" in err &&
|
|
402
|
-
typeof err.code === "string") {
|
|
403
|
-
return err.code;
|
|
404
|
-
}
|
|
405
|
-
return undefined;
|
|
340
|
+
if (typeof err === "object" && err !== null && "code" in err && typeof err.code === "string") return err.code;
|
|
406
341
|
};
|
|
407
342
|
const writeSymlink = (dest, target, onWarn) => {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
writeFileSync(dest, target);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
throw err;
|
|
423
|
-
}
|
|
343
|
+
if (isSymlink(dest)) rmSync(dest, { force: true });
|
|
344
|
+
try {
|
|
345
|
+
symlinkSync(target, dest);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
if (errnoCode(err) === "EPERM" || process.platform === "win32") {
|
|
348
|
+
onWarn?.(`Could not create symlink ${dest} -> ${target}; wrote it as a regular file instead.`);
|
|
349
|
+
writeFileSync(dest, target);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
424
354
|
};
|
|
425
355
|
/**
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return files.length;
|
|
356
|
+
* Download `template` and materialize its files into `targetDir`, creating
|
|
357
|
+
* parent directories, preserving executable bits, and recreating symlinks
|
|
358
|
+
* (with a graceful regular-file fallback on platforms that disallow them).
|
|
359
|
+
* Returns the number of files written. The caller is responsible for any
|
|
360
|
+
* target validation ({@link ensureTargetUsable}) and user-facing progress.
|
|
361
|
+
*/
|
|
362
|
+
const scaffoldTemplate = async (template, targetDir, options = {}) => {
|
|
363
|
+
const files = await downloadTemplate(template);
|
|
364
|
+
mkdirSync(targetDir, { recursive: true });
|
|
365
|
+
for (const file of files) {
|
|
366
|
+
const dest = join(targetDir, file.path);
|
|
367
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
368
|
+
if (file.kind === "symlink") writeSymlink(dest, file.target, options.onWarn);
|
|
369
|
+
else {
|
|
370
|
+
writeFileSync(dest, file.bytes);
|
|
371
|
+
if (file.executable) chmodSync(dest, 493);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return files.length;
|
|
448
375
|
};
|
|
376
|
+
//#endregion
|
|
377
|
+
export { BootstrapInputError, FALLBACK_TEMPLATES, downloadTemplate, ensureTargetUsable, fetchTemplates, findTemplate, parseManifest, parseTar, scaffoldTemplate, selectTemplateFiles, templateIds };
|