@siteknock/cli 0.1.6
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 +70 -0
- package/cli.d.ts +1 -0
- package/cli.js +2223 -0
- package/package.json +23 -0
package/cli.js
ADDED
|
@@ -0,0 +1,2223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
|
|
5
|
+
import path12 from "path";
|
|
6
|
+
import process6 from "process";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
|
+
|
|
9
|
+
// src/commands/app.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
import path2 from "path";
|
|
13
|
+
import process2 from "process";
|
|
14
|
+
import { parseArgs } from "util";
|
|
15
|
+
|
|
16
|
+
// src/lib/workspace.ts
|
|
17
|
+
import { createHash } from "crypto";
|
|
18
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
19
|
+
import path from "path";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
var WORKSPACE_CONFIG_FILENAME = "sk-cli.json";
|
|
22
|
+
var projectFolderNameSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
23
|
+
var workspaceLicenseSchema = z.object({
|
|
24
|
+
token: z.string().min(1),
|
|
25
|
+
updatedAt: z.string().min(1).optional()
|
|
26
|
+
});
|
|
27
|
+
var workspaceProjectInputSchema = z.object({
|
|
28
|
+
path: z.string().min(1).optional(),
|
|
29
|
+
createdAt: z.string().min(1).optional(),
|
|
30
|
+
updatedAt: z.string().min(1).optional(),
|
|
31
|
+
bundleVersion: z.string().min(1).optional(),
|
|
32
|
+
licenseId: z.string().min(1).optional()
|
|
33
|
+
});
|
|
34
|
+
var workspaceProjectSchema = z.object({
|
|
35
|
+
path: z.string().min(1),
|
|
36
|
+
createdAt: z.string().min(1),
|
|
37
|
+
updatedAt: z.string().min(1),
|
|
38
|
+
bundleVersion: z.string().min(1).optional(),
|
|
39
|
+
licenseId: z.string().min(1).optional()
|
|
40
|
+
});
|
|
41
|
+
var studioModeSchema = z.enum(["docker", "app"]);
|
|
42
|
+
var workspaceManifestInputSchema = z.object({
|
|
43
|
+
version: z.literal(1),
|
|
44
|
+
studio: z.object({
|
|
45
|
+
defaultProject: projectFolderNameSchema.optional(),
|
|
46
|
+
mode: studioModeSchema.optional(),
|
|
47
|
+
image: z.string().min(1).optional(),
|
|
48
|
+
appInstallUrl: z.string().url().optional(),
|
|
49
|
+
port: z.number().int().positive().max(65535).optional(),
|
|
50
|
+
containerName: z.string().min(1).optional()
|
|
51
|
+
}).default({}),
|
|
52
|
+
siteknockKb: z.object({
|
|
53
|
+
path: z.string().min(1),
|
|
54
|
+
version: z.string().min(1),
|
|
55
|
+
updatedAt: z.string().min(1)
|
|
56
|
+
}).optional(),
|
|
57
|
+
licenses: z.record(z.string().min(1), workspaceLicenseSchema).default({}),
|
|
58
|
+
projects: z.record(projectFolderNameSchema, workspaceProjectInputSchema).default({})
|
|
59
|
+
});
|
|
60
|
+
var workspaceManifestSchema = z.object({
|
|
61
|
+
version: z.literal(1),
|
|
62
|
+
studio: z.object({
|
|
63
|
+
defaultProject: projectFolderNameSchema.optional(),
|
|
64
|
+
mode: studioModeSchema.optional(),
|
|
65
|
+
image: z.string().min(1).optional(),
|
|
66
|
+
appInstallUrl: z.string().url().optional(),
|
|
67
|
+
port: z.number().int().positive().max(65535).optional(),
|
|
68
|
+
containerName: z.string().min(1).optional()
|
|
69
|
+
}).default({}),
|
|
70
|
+
siteknockKb: z.object({
|
|
71
|
+
path: z.string().min(1),
|
|
72
|
+
version: z.string().min(1),
|
|
73
|
+
updatedAt: z.string().min(1)
|
|
74
|
+
}).optional(),
|
|
75
|
+
licenses: z.record(z.string().min(1), workspaceLicenseSchema).default({}),
|
|
76
|
+
projects: z.record(projectFolderNameSchema, workspaceProjectSchema).default({})
|
|
77
|
+
});
|
|
78
|
+
function createDefaultWorkspaceManifest() {
|
|
79
|
+
return {
|
|
80
|
+
version: 1,
|
|
81
|
+
studio: {},
|
|
82
|
+
licenses: {},
|
|
83
|
+
projects: {}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function createWorkspaceLicenseId(input3) {
|
|
87
|
+
return createHash("sha256").update(input3.token).digest("hex");
|
|
88
|
+
}
|
|
89
|
+
function pruneUnusedLicenses(licenses, projects) {
|
|
90
|
+
const activeLicenseIds = new Set(
|
|
91
|
+
Object.values(projects).map((project) => project.licenseId).filter((licenseId) => typeof licenseId === "string" && licenseId.length > 0)
|
|
92
|
+
);
|
|
93
|
+
return Object.fromEntries(Object.entries(licenses).filter(([licenseId]) => activeLicenseIds.has(licenseId)));
|
|
94
|
+
}
|
|
95
|
+
async function resolveWorkspaceRoot(startDir) {
|
|
96
|
+
let currentDir = path.resolve(startDir);
|
|
97
|
+
for (; ; ) {
|
|
98
|
+
const configPath = path.join(currentDir, WORKSPACE_CONFIG_FILENAME);
|
|
99
|
+
if (await fileExists(configPath)) {
|
|
100
|
+
return currentDir;
|
|
101
|
+
}
|
|
102
|
+
const parentDir = path.dirname(currentDir);
|
|
103
|
+
if (parentDir === currentDir) {
|
|
104
|
+
return path.resolve(startDir);
|
|
105
|
+
}
|
|
106
|
+
currentDir = parentDir;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function readWorkspaceManifest(workspaceRoot) {
|
|
110
|
+
const configPath = getWorkspaceConfigPath(workspaceRoot);
|
|
111
|
+
try {
|
|
112
|
+
const raw = await readFile(configPath, "utf8");
|
|
113
|
+
const parsed = workspaceManifestInputSchema.parse(JSON.parse(raw));
|
|
114
|
+
return normalizeWorkspaceManifest(parsed);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (isMissingFileError(error)) {
|
|
117
|
+
return createDefaultWorkspaceManifest();
|
|
118
|
+
}
|
|
119
|
+
if (error instanceof z.ZodError) {
|
|
120
|
+
throw new Error(`Invalid ${WORKSPACE_CONFIG_FILENAME}: ${error.issues.map((issue) => issue.message).join("; ")}`);
|
|
121
|
+
}
|
|
122
|
+
if (error instanceof SyntaxError) {
|
|
123
|
+
throw new Error(`Invalid JSON in ${WORKSPACE_CONFIG_FILENAME}`);
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function writeWorkspaceManifest(workspaceRoot, manifest) {
|
|
129
|
+
const configPath = getWorkspaceConfigPath(workspaceRoot);
|
|
130
|
+
await mkdir(workspaceRoot, { recursive: true });
|
|
131
|
+
await writeFile(configPath, `${JSON.stringify(workspaceManifestSchema.parse(manifest), null, 2)}
|
|
132
|
+
`, {
|
|
133
|
+
encoding: "utf8",
|
|
134
|
+
mode: 384
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function getWorkspaceConfigPath(workspaceRoot) {
|
|
138
|
+
return path.join(workspaceRoot, WORKSPACE_CONFIG_FILENAME);
|
|
139
|
+
}
|
|
140
|
+
function resolveTopLevelProjectDirectory(workspaceRoot, rawProjectDir) {
|
|
141
|
+
const trimmed = rawProjectDir.trim();
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
throw new Error("Missing project folder. Pass --project <folder-name> or -p <folder-name>.");
|
|
144
|
+
}
|
|
145
|
+
const resolvedPath = path.resolve(workspaceRoot, trimmed);
|
|
146
|
+
const relativePath = path.relative(workspaceRoot, resolvedPath);
|
|
147
|
+
const segments = relativePath.split(path.sep).filter(Boolean);
|
|
148
|
+
if (relativePath === "" || relativePath.startsWith("..") || path.isAbsolute(relativePath) || segments.length !== 1) {
|
|
149
|
+
throw new Error(`Project folder must be a direct child of ${workspaceRoot}`);
|
|
150
|
+
}
|
|
151
|
+
const [projectName] = segments;
|
|
152
|
+
projectFolderNameSchema.parse(projectName);
|
|
153
|
+
return {
|
|
154
|
+
projectName,
|
|
155
|
+
projectDir: path.join(workspaceRoot, projectName)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function getProjectNameFromDirectory(workspaceRoot, projectDir) {
|
|
159
|
+
return resolveTopLevelProjectDirectory(workspaceRoot, path.relative(workspaceRoot, projectDir)).projectName;
|
|
160
|
+
}
|
|
161
|
+
function listWorkspaceProjects(manifest) {
|
|
162
|
+
return Object.keys(manifest.projects).sort((left, right) => left.localeCompare(right));
|
|
163
|
+
}
|
|
164
|
+
function readProjectActivation(manifest, projectName) {
|
|
165
|
+
const licenseId = manifest.projects[projectName]?.licenseId;
|
|
166
|
+
if (!licenseId) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return manifest.licenses[licenseId] ?? null;
|
|
170
|
+
}
|
|
171
|
+
function resolvePreferredProject(manifest) {
|
|
172
|
+
const configured = manifest.studio.defaultProject;
|
|
173
|
+
if (configured && manifest.projects[configured]) {
|
|
174
|
+
return configured;
|
|
175
|
+
}
|
|
176
|
+
return listWorkspaceProjects(manifest)[0] ?? null;
|
|
177
|
+
}
|
|
178
|
+
function withStudioSettings(manifest, input3) {
|
|
179
|
+
return {
|
|
180
|
+
...manifest,
|
|
181
|
+
studio: {
|
|
182
|
+
...manifest.studio,
|
|
183
|
+
...input3.defaultProject ? { defaultProject: input3.defaultProject } : {},
|
|
184
|
+
...input3.mode ? { mode: input3.mode } : {},
|
|
185
|
+
...input3.image ? { image: input3.image } : {},
|
|
186
|
+
...input3.appInstallUrl ? { appInstallUrl: input3.appInstallUrl } : {},
|
|
187
|
+
...typeof input3.port === "number" ? { port: input3.port } : {},
|
|
188
|
+
...input3.containerName ? { containerName: input3.containerName } : {}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function withRegisteredProject(manifest, input3) {
|
|
193
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
194
|
+
const existing = manifest.projects[input3.projectName];
|
|
195
|
+
const licenseId = input3.activation ? createWorkspaceLicenseId(input3.activation) : existing?.licenseId;
|
|
196
|
+
const nextProject = {
|
|
197
|
+
path: input3.projectName,
|
|
198
|
+
createdAt: existing?.createdAt ?? now,
|
|
199
|
+
updatedAt: now,
|
|
200
|
+
...input3.bundleVersion ? { bundleVersion: input3.bundleVersion } : existing?.bundleVersion ? { bundleVersion: existing.bundleVersion } : {},
|
|
201
|
+
...licenseId ? { licenseId } : {}
|
|
202
|
+
};
|
|
203
|
+
const nextProjects = {
|
|
204
|
+
...manifest.projects,
|
|
205
|
+
[input3.projectName]: nextProject
|
|
206
|
+
};
|
|
207
|
+
const nextLicenses = input3.activation && licenseId ? {
|
|
208
|
+
...manifest.licenses,
|
|
209
|
+
[licenseId]: {
|
|
210
|
+
...manifest.licenses[licenseId] ?? {},
|
|
211
|
+
token: input3.activation.token,
|
|
212
|
+
updatedAt: now
|
|
213
|
+
}
|
|
214
|
+
} : manifest.licenses;
|
|
215
|
+
return {
|
|
216
|
+
...manifest,
|
|
217
|
+
licenses: pruneUnusedLicenses(nextLicenses, nextProjects),
|
|
218
|
+
projects: nextProjects,
|
|
219
|
+
studio: {
|
|
220
|
+
...manifest.studio,
|
|
221
|
+
...input3.selected === true ? { defaultProject: input3.projectName } : {}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function withSiteknockKb(manifest, input3) {
|
|
226
|
+
return {
|
|
227
|
+
...manifest,
|
|
228
|
+
siteknockKb: {
|
|
229
|
+
path: input3.path,
|
|
230
|
+
version: input3.version,
|
|
231
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function normalizeWorkspaceManifest(manifest) {
|
|
236
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
237
|
+
const projects = Object.fromEntries(
|
|
238
|
+
Object.entries(manifest.projects).map(([projectName, project]) => {
|
|
239
|
+
const createdAt = project.createdAt ?? project.updatedAt ?? now;
|
|
240
|
+
const updatedAt = project.updatedAt ?? project.createdAt ?? now;
|
|
241
|
+
const licenseId = project.licenseId && manifest.licenses[project.licenseId] ? project.licenseId : void 0;
|
|
242
|
+
return [
|
|
243
|
+
projectName,
|
|
244
|
+
{
|
|
245
|
+
path: project.path ?? projectName,
|
|
246
|
+
createdAt,
|
|
247
|
+
updatedAt,
|
|
248
|
+
...project.bundleVersion ? { bundleVersion: project.bundleVersion } : {},
|
|
249
|
+
...licenseId ? { licenseId } : {}
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
return workspaceManifestSchema.parse({
|
|
255
|
+
...manifest,
|
|
256
|
+
licenses: pruneUnusedLicenses(manifest.licenses, projects),
|
|
257
|
+
projects
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function isMissingFileError(error) {
|
|
261
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
262
|
+
}
|
|
263
|
+
async function fileExists(filePath) {
|
|
264
|
+
try {
|
|
265
|
+
await readFile(filePath, "utf8");
|
|
266
|
+
return true;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (isMissingFileError(error)) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/commands/app.ts
|
|
276
|
+
var appFlags = {
|
|
277
|
+
project: { type: "string", short: "p" }
|
|
278
|
+
};
|
|
279
|
+
async function runAppCommand(argv) {
|
|
280
|
+
const subArgs = argv.slice(1);
|
|
281
|
+
const subcommand = subArgs.find((value) => !value.startsWith("-"));
|
|
282
|
+
if (!subcommand || subcommand === "help") {
|
|
283
|
+
printAppHelp();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const parsed = parseArgs({ args: subArgs, options: appFlags, allowPositionals: true, strict: false });
|
|
287
|
+
const projectDir = await resolveProjectDir(parsed.values.project);
|
|
288
|
+
const appCliPath = path2.join(projectDir, "node_modules", "@siteknock", "generator", "app-cli.mjs");
|
|
289
|
+
if (!existsSync(appCliPath)) {
|
|
290
|
+
throw new Error(`Could not find @siteknock/generator in ${projectDir}. Run install in the project first.`);
|
|
291
|
+
}
|
|
292
|
+
const forwarded = stripProjectFlag(subArgs);
|
|
293
|
+
await delegate(appCliPath, projectDir, forwarded);
|
|
294
|
+
}
|
|
295
|
+
async function resolveProjectDir(projectFlag) {
|
|
296
|
+
const workspaceRoot = await resolveWorkspaceRoot(process2.cwd());
|
|
297
|
+
if (projectFlag) {
|
|
298
|
+
return resolveTopLevelProjectDirectory(workspaceRoot, projectFlag).projectDir;
|
|
299
|
+
}
|
|
300
|
+
if (existsSync(path2.join(process2.cwd(), "config", "sk-config.jsonc"))) {
|
|
301
|
+
return process2.cwd();
|
|
302
|
+
}
|
|
303
|
+
const manifest = await readWorkspaceManifest(workspaceRoot);
|
|
304
|
+
const defaultProject = manifest.studio.defaultProject;
|
|
305
|
+
if (defaultProject) {
|
|
306
|
+
return resolveTopLevelProjectDirectory(workspaceRoot, defaultProject).projectDir;
|
|
307
|
+
}
|
|
308
|
+
throw new Error("No project specified. Pass --project <folder> or run inside a project directory.");
|
|
309
|
+
}
|
|
310
|
+
function stripProjectFlag(args) {
|
|
311
|
+
const out = [];
|
|
312
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
313
|
+
const arg = args[i];
|
|
314
|
+
if (arg === "--project" || arg === "-p") {
|
|
315
|
+
i += 1;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (arg.startsWith("--project=")) continue;
|
|
319
|
+
out.push(arg);
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
function delegate(appCliPath, projectDir, args) {
|
|
324
|
+
return new Promise((resolve, reject) => {
|
|
325
|
+
const child = spawn("node", [appCliPath, ...args], { cwd: projectDir, stdio: "inherit" });
|
|
326
|
+
child.on("error", reject);
|
|
327
|
+
child.on("exit", (code) => {
|
|
328
|
+
if (code === 0) resolve();
|
|
329
|
+
else reject(new Error(`app command exited with code ${code ?? "null"}`));
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function printAppHelp() {
|
|
334
|
+
console.log(`
|
|
335
|
+
siteknock app \u2014 manage apps within a project
|
|
336
|
+
|
|
337
|
+
Usage:
|
|
338
|
+
siteknock app create --project <folder> --name <n> --type <fe|be|fe+be|e2e> [options]
|
|
339
|
+
siteknock app list --project <folder>
|
|
340
|
+
siteknock app remove --project <folder> --name <n>
|
|
341
|
+
|
|
342
|
+
Create options:
|
|
343
|
+
--targets a,b e2e: comma-separated target app names
|
|
344
|
+
--frontend <folder> override frontend folder name
|
|
345
|
+
--backend <folder> override backend folder name
|
|
346
|
+
--fe-port <n> frontend port
|
|
347
|
+
--be-port <n> backend port
|
|
348
|
+
--no-generate skip running the generator after creation
|
|
349
|
+
`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/commands/create.ts
|
|
353
|
+
import { mkdir as mkdir3, mkdtemp as mkdtemp2, rm as rm2, stat } from "fs/promises";
|
|
354
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
355
|
+
import { spawn as spawn2 } from "child_process";
|
|
356
|
+
import path7 from "path";
|
|
357
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
358
|
+
import semver from "semver";
|
|
359
|
+
import { z as z4 } from "zod";
|
|
360
|
+
|
|
361
|
+
// src/lib/config.ts
|
|
362
|
+
import path3 from "path";
|
|
363
|
+
import process3 from "process";
|
|
364
|
+
import readline from "readline/promises";
|
|
365
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
366
|
+
import { stdin as input, stdout as output } from "process";
|
|
367
|
+
import { fileURLToPath } from "url";
|
|
368
|
+
import { parseEnv } from "util";
|
|
369
|
+
var DEV_LICENSE_URL_ENV = "SK_DEV_LICENSE_URL";
|
|
370
|
+
var DEV_REGISTRY_URL_ENV = "SK_DEV_REGISTRY_URL";
|
|
371
|
+
var DEV_GENERATOR_PACKAGE_ENV = "SK_DEV_GENERATOR_PACKAGE";
|
|
372
|
+
var PRODUCTION_LICENSE_URL = "https://api.siteknock.com";
|
|
373
|
+
var PRODUCTION_REGISTRY_URL = "https://registry.siteknock.com";
|
|
374
|
+
var PRODUCTION_GENERATOR_PACKAGE = "@siteknock/generator";
|
|
375
|
+
function loadEnvFile(explicitPath) {
|
|
376
|
+
if (!isInternalDevelopmentMode() || !explicitPath) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const layeredEnv = readEnvFile(explicitPath);
|
|
380
|
+
for (const [key, value] of Object.entries(layeredEnv)) {
|
|
381
|
+
if (process3.env[key] === void 0) {
|
|
382
|
+
process3.env[key] = value;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function getConfig(input3) {
|
|
387
|
+
const workspaceRoot = await resolveWorkspaceRoot(input3.workspaceRoot ?? process3.cwd());
|
|
388
|
+
const manifest = await readWorkspaceManifest(workspaceRoot);
|
|
389
|
+
const selectedProjectName = input3.projectName ?? resolvePreferredProject(manifest) ?? void 0;
|
|
390
|
+
const projectDir = input3.projectDir ? resolveTopLevelProjectDirectory(workspaceRoot, input3.projectDir).projectDir : selectedProjectName ? path3.join(workspaceRoot, selectedProjectName) : workspaceRoot;
|
|
391
|
+
const licenseUrl = stripTrailingSlash(resolveLicenseUrl());
|
|
392
|
+
const registryUrl = stripTrailingSlash(resolveRegistryUrl());
|
|
393
|
+
const generatorPackage = resolveGeneratorPackage();
|
|
394
|
+
const manifestToken = selectedProjectName ? readProjectActivation(manifest, selectedProjectName)?.token : void 0;
|
|
395
|
+
return {
|
|
396
|
+
token: input3.token ?? manifestToken ?? process3.env.SITEKNOCK_TOKEN,
|
|
397
|
+
workspaceRoot,
|
|
398
|
+
projectDir,
|
|
399
|
+
licenseUrl,
|
|
400
|
+
registryUrl,
|
|
401
|
+
generatorPackage,
|
|
402
|
+
requestedVersion: input3.requestedVersion,
|
|
403
|
+
yes: input3.yes === true
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async function promptForMissingFields(config) {
|
|
407
|
+
return promptForMissingFieldsWithOptions(config);
|
|
408
|
+
}
|
|
409
|
+
async function promptForMissingFieldsWithOptions(config, options) {
|
|
410
|
+
if (config.yes) {
|
|
411
|
+
return requireFields(config, options);
|
|
412
|
+
}
|
|
413
|
+
const rl = readline.createInterface({ input, output });
|
|
414
|
+
try {
|
|
415
|
+
const shouldAskForProjectFolder = !options?.skipProjectDirPrompt && config.projectDir === config.workspaceRoot;
|
|
416
|
+
const projectDirInput = shouldAskForProjectFolder ? await rl.question("Project folder: ") : "";
|
|
417
|
+
const projectDir = projectDirInput.trim() ? resolveTopLevelProjectDirectory(config.workspaceRoot, projectDirInput.trim()).projectDir : config.projectDir;
|
|
418
|
+
const shouldContinue = await rl.question(
|
|
419
|
+
"Continue with SiteKnock browser sign-in + entitlement verification? [y/N]: "
|
|
420
|
+
);
|
|
421
|
+
if (!/^y(es)?$/i.test(shouldContinue.trim())) {
|
|
422
|
+
throw new Error("Cancelled by user");
|
|
423
|
+
}
|
|
424
|
+
return requireFields(
|
|
425
|
+
{
|
|
426
|
+
...config,
|
|
427
|
+
projectDir
|
|
428
|
+
},
|
|
429
|
+
options
|
|
430
|
+
);
|
|
431
|
+
} finally {
|
|
432
|
+
rl.close();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function requireFields(config, options) {
|
|
436
|
+
if (!options?.skipProjectDirPrompt && (!config.projectDir || config.projectDir === config.workspaceRoot)) {
|
|
437
|
+
throw new Error("Missing project folder. Pass --project <folder-name> or -p <folder-name>.");
|
|
438
|
+
}
|
|
439
|
+
if (!config.licenseUrl) throw new Error("Missing built-in licensing API URL.");
|
|
440
|
+
if (!config.registryUrl) throw new Error("Missing built-in registry URL.");
|
|
441
|
+
if (!config.generatorPackage) throw new Error("Missing built-in generator package.");
|
|
442
|
+
return {
|
|
443
|
+
token: config.token,
|
|
444
|
+
workspaceRoot: config.workspaceRoot,
|
|
445
|
+
projectDir: config.projectDir,
|
|
446
|
+
licenseUrl: config.licenseUrl,
|
|
447
|
+
registryUrl: config.registryUrl,
|
|
448
|
+
generatorPackage: config.generatorPackage,
|
|
449
|
+
requestedVersion: config.requestedVersion ?? "",
|
|
450
|
+
yes: config.yes
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function stripTrailingSlash(value) {
|
|
454
|
+
return value.replace(/\/+$/, "");
|
|
455
|
+
}
|
|
456
|
+
function readHiddenOverride(envName) {
|
|
457
|
+
if (!isInternalDevelopmentMode()) {
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
const value = process3.env[envName]?.trim();
|
|
461
|
+
return value ? value : void 0;
|
|
462
|
+
}
|
|
463
|
+
function findInternalDevelopmentRoot() {
|
|
464
|
+
const moduleDir = path3.dirname(fileURLToPath(import.meta.url));
|
|
465
|
+
const packageRoot = findNearestPackageRoot(moduleDir);
|
|
466
|
+
if (!packageRoot) {
|
|
467
|
+
return void 0;
|
|
468
|
+
}
|
|
469
|
+
if (existsSync2(path3.join(packageRoot, "src", "cli.ts")) && existsSync2(path3.join(packageRoot, ".env.example"))) {
|
|
470
|
+
return packageRoot;
|
|
471
|
+
}
|
|
472
|
+
return void 0;
|
|
473
|
+
}
|
|
474
|
+
function isInternalDevelopmentMode() {
|
|
475
|
+
return findInternalDevelopmentRoot() !== void 0;
|
|
476
|
+
}
|
|
477
|
+
function findNearestPackageRoot(startDir) {
|
|
478
|
+
let currentDir = path3.resolve(startDir);
|
|
479
|
+
for (; ; ) {
|
|
480
|
+
if (existsSync2(path3.join(currentDir, "package.json"))) {
|
|
481
|
+
return currentDir;
|
|
482
|
+
}
|
|
483
|
+
const parentDir = path3.dirname(currentDir);
|
|
484
|
+
if (parentDir === currentDir) {
|
|
485
|
+
return void 0;
|
|
486
|
+
}
|
|
487
|
+
currentDir = parentDir;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function readEnvFile(filePath) {
|
|
491
|
+
if (!filePath || !existsSync2(filePath)) {
|
|
492
|
+
return {};
|
|
493
|
+
}
|
|
494
|
+
const parsed = parseEnv(readFileSync(filePath, "utf8"));
|
|
495
|
+
return Object.fromEntries(
|
|
496
|
+
Object.entries(parsed).filter((entry) => typeof entry[1] === "string")
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
function resolveLicenseUrl() {
|
|
500
|
+
return readHiddenOverride(DEV_LICENSE_URL_ENV) ?? readDevelopmentEnv("SK_LICENSE_API_URL") ?? PRODUCTION_LICENSE_URL;
|
|
501
|
+
}
|
|
502
|
+
function resolveRegistryUrl() {
|
|
503
|
+
return readHiddenOverride(DEV_REGISTRY_URL_ENV) ?? readDevelopmentEnv("SK_REGISTRY_URL") ?? PRODUCTION_REGISTRY_URL;
|
|
504
|
+
}
|
|
505
|
+
function resolveGeneratorPackage() {
|
|
506
|
+
return readHiddenOverride(DEV_GENERATOR_PACKAGE_ENV) ?? readDevelopmentEnv("SK_GENERATOR_PACKAGE") ?? PRODUCTION_GENERATOR_PACKAGE;
|
|
507
|
+
}
|
|
508
|
+
function readDevelopmentEnv(envName) {
|
|
509
|
+
if (!isInternalDevelopmentMode()) {
|
|
510
|
+
return void 0;
|
|
511
|
+
}
|
|
512
|
+
const value = process3.env[envName]?.trim();
|
|
513
|
+
return value ? value : void 0;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/lib/http.ts
|
|
517
|
+
import { z as z2 } from "zod";
|
|
518
|
+
var authorizedLicenseSchema = z2.object({
|
|
519
|
+
status: z2.literal("approved"),
|
|
520
|
+
token: z2.string().min(1),
|
|
521
|
+
customer_name: z2.string(),
|
|
522
|
+
plan: z2.string(),
|
|
523
|
+
min_version: z2.string(),
|
|
524
|
+
max_version: z2.string(),
|
|
525
|
+
project_seats: z2.number(),
|
|
526
|
+
seat_index: z2.number().int().nullable().optional(),
|
|
527
|
+
licensing_document: z2.unknown().nullable().optional(),
|
|
528
|
+
expires_at: z2.string().nullable().optional(),
|
|
529
|
+
registry_url: z2.string().url().optional(),
|
|
530
|
+
generator_package: z2.string().min(1).optional(),
|
|
531
|
+
studio_image: z2.string().min(1).optional(),
|
|
532
|
+
studio_app_install_url: z2.string().url().nullable().optional()
|
|
533
|
+
});
|
|
534
|
+
var deviceAuthorizationStartSchema = z2.object({
|
|
535
|
+
device_code: z2.string().min(1),
|
|
536
|
+
user_code: z2.string().min(1),
|
|
537
|
+
verification_uri: z2.string().url(),
|
|
538
|
+
verification_uri_complete: z2.string().url(),
|
|
539
|
+
interval: z2.number().int().positive(),
|
|
540
|
+
expires_at: z2.string()
|
|
541
|
+
});
|
|
542
|
+
var deviceAuthorizationPollSchema = z2.discriminatedUnion("status", [
|
|
543
|
+
z2.object({
|
|
544
|
+
status: z2.literal("pending")
|
|
545
|
+
}),
|
|
546
|
+
z2.object({
|
|
547
|
+
status: z2.literal("expired"),
|
|
548
|
+
error: z2.string().optional()
|
|
549
|
+
}),
|
|
550
|
+
authorizedLicenseSchema
|
|
551
|
+
]);
|
|
552
|
+
var entitlementSchema = z2.object({
|
|
553
|
+
valid: z2.literal(true),
|
|
554
|
+
customer_name: z2.string(),
|
|
555
|
+
plan: z2.string(),
|
|
556
|
+
min_version: z2.string(),
|
|
557
|
+
max_version: z2.string(),
|
|
558
|
+
cli_seats: z2.number().int().positive(),
|
|
559
|
+
studio_seats: z2.number().int().positive(),
|
|
560
|
+
renewal_active: z2.boolean(),
|
|
561
|
+
expires_at: z2.string().nullable().optional()
|
|
562
|
+
});
|
|
563
|
+
async function startDeviceAuthorization(input3) {
|
|
564
|
+
const response = await fetch(`${input3.licenseUrl}/api/cli/connect/start`, {
|
|
565
|
+
method: "POST",
|
|
566
|
+
headers: { "Content-Type": "application/json" },
|
|
567
|
+
body: JSON.stringify({
|
|
568
|
+
requested_version: input3.requestedVersion ?? null,
|
|
569
|
+
purpose: input3.purpose ?? "create",
|
|
570
|
+
public_key: input3.publicKey ?? null,
|
|
571
|
+
machine_hash: input3.machineHash ?? null,
|
|
572
|
+
app_path_hash: input3.appPathHash ?? null,
|
|
573
|
+
project_name: input3.projectName ?? null,
|
|
574
|
+
workspace_name: input3.workspaceName ?? null,
|
|
575
|
+
workspace_path: input3.workspacePath ?? null
|
|
576
|
+
})
|
|
577
|
+
});
|
|
578
|
+
const body = await parseJson(response);
|
|
579
|
+
if (!response.ok) {
|
|
580
|
+
throw new Error(extractApiError(body, "Device authorization could not be started"));
|
|
581
|
+
}
|
|
582
|
+
const parsed = deviceAuthorizationStartSchema.parse(body);
|
|
583
|
+
return {
|
|
584
|
+
deviceCode: parsed.device_code,
|
|
585
|
+
userCode: parsed.user_code,
|
|
586
|
+
verificationUri: parsed.verification_uri,
|
|
587
|
+
verificationUriComplete: parsed.verification_uri_complete,
|
|
588
|
+
intervalSeconds: parsed.interval,
|
|
589
|
+
expiresAt: parsed.expires_at
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
async function pollDeviceAuthorization(input3) {
|
|
593
|
+
const response = await fetch(
|
|
594
|
+
`${input3.licenseUrl}/api/cli/connect/poll?device_code=${encodeURIComponent(input3.deviceCode)}`,
|
|
595
|
+
{
|
|
596
|
+
method: "GET"
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
const body = await parseJson(response);
|
|
600
|
+
if (!response.ok && response.status !== 404) {
|
|
601
|
+
throw new Error(extractApiError(body, "Device authorization polling failed"));
|
|
602
|
+
}
|
|
603
|
+
const parsed = deviceAuthorizationPollSchema.parse(body);
|
|
604
|
+
if (parsed.status === "pending") {
|
|
605
|
+
return { status: "pending" };
|
|
606
|
+
}
|
|
607
|
+
if (parsed.status === "expired") {
|
|
608
|
+
return {
|
|
609
|
+
status: "expired",
|
|
610
|
+
reason: parsed.error ?? "Authorization expired before approval completed"
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
status: "approved",
|
|
615
|
+
token: parsed.token,
|
|
616
|
+
customerName: parsed.customer_name,
|
|
617
|
+
plan: parsed.plan,
|
|
618
|
+
minVersion: parsed.min_version,
|
|
619
|
+
maxVersion: parsed.max_version,
|
|
620
|
+
projectSeats: parsed.project_seats,
|
|
621
|
+
seatIndex: parsed.seat_index ?? null,
|
|
622
|
+
licensingDocument: parsed.licensing_document ?? null,
|
|
623
|
+
expiresAt: parsed.expires_at ?? null,
|
|
624
|
+
registryUrl: parsed.registry_url,
|
|
625
|
+
generatorPackage: parsed.generator_package,
|
|
626
|
+
studioImage: parsed.studio_image,
|
|
627
|
+
studioAppInstallUrl: parsed.studio_app_install_url ?? null
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
async function getEntitlement(input3) {
|
|
631
|
+
const response = await fetch(`${input3.licenseUrl}/api/entitlement`, {
|
|
632
|
+
method: "GET",
|
|
633
|
+
headers: {
|
|
634
|
+
Authorization: `Bearer ${input3.token}`
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
const body = await parseJson(response);
|
|
638
|
+
if (!response.ok) {
|
|
639
|
+
throw new Error(extractApiError(body, "Entitlement check failed"));
|
|
640
|
+
}
|
|
641
|
+
const parsed = entitlementSchema.parse(body);
|
|
642
|
+
return {
|
|
643
|
+
customerName: parsed.customer_name,
|
|
644
|
+
plan: parsed.plan,
|
|
645
|
+
minVersion: parsed.min_version,
|
|
646
|
+
maxVersion: parsed.max_version,
|
|
647
|
+
cliSeats: parsed.cli_seats,
|
|
648
|
+
studioSeats: parsed.studio_seats,
|
|
649
|
+
renewalActive: parsed.renewal_active,
|
|
650
|
+
expiresAt: parsed.expires_at ?? null
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
async function parseJson(response) {
|
|
654
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
655
|
+
if (!contentType.includes("application/json")) {
|
|
656
|
+
const text = await response.text();
|
|
657
|
+
return { error: text };
|
|
658
|
+
}
|
|
659
|
+
return response.json();
|
|
660
|
+
}
|
|
661
|
+
function extractApiError(body, fallback) {
|
|
662
|
+
if (typeof body === "object" && body !== null) {
|
|
663
|
+
const candidate = body;
|
|
664
|
+
const reason = candidate.reason;
|
|
665
|
+
const message = candidate.message;
|
|
666
|
+
const error = candidate.error;
|
|
667
|
+
if (typeof reason === "string" && reason) return reason;
|
|
668
|
+
if (typeof message === "string" && message) return message;
|
|
669
|
+
if (typeof error === "string" && error) return error;
|
|
670
|
+
}
|
|
671
|
+
return fallback;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/lib/device-auth.ts
|
|
675
|
+
async function authorizeLicenseWithBrowser(config) {
|
|
676
|
+
const session = await startDeviceAuthorization({
|
|
677
|
+
licenseUrl: config.licenseUrl,
|
|
678
|
+
requestedVersion: config.requestedVersion || void 0,
|
|
679
|
+
purpose: config.purpose ?? "create",
|
|
680
|
+
publicKey: config.publicKey,
|
|
681
|
+
machineHash: config.machineHash,
|
|
682
|
+
appPathHash: config.appPathHash,
|
|
683
|
+
projectName: config.projectName,
|
|
684
|
+
workspaceName: config.workspaceName,
|
|
685
|
+
workspacePath: config.workspacePath
|
|
686
|
+
});
|
|
687
|
+
printAuthorizationInstructions(session);
|
|
688
|
+
for (; ; ) {
|
|
689
|
+
await sleep(session.intervalSeconds * 1e3);
|
|
690
|
+
const result = await pollDeviceAuthorization({
|
|
691
|
+
licenseUrl: config.licenseUrl,
|
|
692
|
+
deviceCode: session.deviceCode
|
|
693
|
+
});
|
|
694
|
+
if (result.status === "pending") {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (result.status === "expired") {
|
|
698
|
+
throw new Error(result.reason);
|
|
699
|
+
}
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function printAuthorizationInstructions(session) {
|
|
704
|
+
console.log("Sign in to SiteKnock in your browser and choose the license to use for this command.");
|
|
705
|
+
console.log(`Open: ${session.verificationUri}`);
|
|
706
|
+
console.log(`Code: ${session.userCode}`);
|
|
707
|
+
console.log(`Direct link: ${session.verificationUriComplete}`);
|
|
708
|
+
console.log(`This request expires at ${session.expiresAt}`);
|
|
709
|
+
console.log("Waiting for approval...");
|
|
710
|
+
}
|
|
711
|
+
async function sleep(ms) {
|
|
712
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/lib/cli-identity.ts
|
|
716
|
+
import crypto2 from "crypto";
|
|
717
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
718
|
+
import path4 from "path";
|
|
719
|
+
import { z as z3 } from "zod";
|
|
720
|
+
|
|
721
|
+
// ../packages/common-utils/src/machine.ts
|
|
722
|
+
import cp from "child_process";
|
|
723
|
+
import fs from "fs";
|
|
724
|
+
import os from "os";
|
|
725
|
+
var cachedMachineFingerprint = null;
|
|
726
|
+
function getMachineFingerprint() {
|
|
727
|
+
if (cachedMachineFingerprint !== null) {
|
|
728
|
+
return cachedMachineFingerprint;
|
|
729
|
+
}
|
|
730
|
+
cachedMachineFingerprint = computeMachineFingerprint();
|
|
731
|
+
return cachedMachineFingerprint;
|
|
732
|
+
}
|
|
733
|
+
function computeMachineFingerprint() {
|
|
734
|
+
const platform = os.platform();
|
|
735
|
+
const arch = os.arch();
|
|
736
|
+
try {
|
|
737
|
+
if (platform === "darwin") {
|
|
738
|
+
const output3 = cp.execSync("ioreg -rd1 -c IOPlatformExpertDevice", {
|
|
739
|
+
encoding: "utf8",
|
|
740
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
741
|
+
timeout: 2e3
|
|
742
|
+
});
|
|
743
|
+
const match = output3.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/i);
|
|
744
|
+
if (match && match[1]) {
|
|
745
|
+
return `${platform}|${arch}|macos-uuid:${match[1].trim()}`;
|
|
746
|
+
}
|
|
747
|
+
} else if (platform === "linux") {
|
|
748
|
+
for (const idPath of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
749
|
+
if (fs.existsSync(idPath)) {
|
|
750
|
+
const id = fs.readFileSync(idPath, "utf8").trim();
|
|
751
|
+
if (id) {
|
|
752
|
+
return `${platform}|${arch}|linux-id:${id}`;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} else if (platform === "win32") {
|
|
757
|
+
const output3 = cp.execSync("reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid", {
|
|
758
|
+
encoding: "utf8",
|
|
759
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
760
|
+
timeout: 2e3
|
|
761
|
+
});
|
|
762
|
+
const match = output3.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i);
|
|
763
|
+
if (match && match[1]) {
|
|
764
|
+
return `${platform}|${arch}|windows-guid:${match[1].trim()}`;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
const interfaces = os.networkInterfaces();
|
|
770
|
+
const macs = [];
|
|
771
|
+
for (const name of Object.keys(interfaces)) {
|
|
772
|
+
const lowerName = name.toLowerCase();
|
|
773
|
+
if (lowerName.startsWith("utun") || lowerName.startsWith("tun") || lowerName.startsWith("tap") || lowerName.startsWith("wg") || // WireGuard
|
|
774
|
+
lowerName.startsWith("ppp")) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const list = interfaces[name];
|
|
778
|
+
if (!list) continue;
|
|
779
|
+
for (const info of list) {
|
|
780
|
+
if (info.mac && info.mac !== "00:00:00:00:00:00" && !info.internal) {
|
|
781
|
+
macs.push(info.mac);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const uniqueMacs = [...new Set(macs)].sort();
|
|
786
|
+
const macPart = uniqueMacs.length > 0 ? uniqueMacs.join(",") : os.homedir();
|
|
787
|
+
return `${platform}|${arch}|fallback-macs:${macPart}`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ../packages/common-utils/src/hash.ts
|
|
791
|
+
import crypto from "crypto";
|
|
792
|
+
function sha256(value, encoding = "hex") {
|
|
793
|
+
return crypto.createHash("sha256").update(value).digest(encoding);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/lib/cli-identity.ts
|
|
797
|
+
var projectKeypairSchema = z3.object({
|
|
798
|
+
keyId: z3.string().min(1),
|
|
799
|
+
publicKey: z3.string().min(1),
|
|
800
|
+
privateKey: z3.string().min(1),
|
|
801
|
+
createdAt: z3.string().min(1)
|
|
802
|
+
});
|
|
803
|
+
function hashValue(value) {
|
|
804
|
+
return sha256(value);
|
|
805
|
+
}
|
|
806
|
+
function identityDir(projectDir) {
|
|
807
|
+
return path4.join(projectDir, ".siteknock");
|
|
808
|
+
}
|
|
809
|
+
function keypairPath(projectDir) {
|
|
810
|
+
return path4.join(identityDir(projectDir), "cli-identity.json");
|
|
811
|
+
}
|
|
812
|
+
function generateProjectKeypair() {
|
|
813
|
+
const { publicKey, privateKey } = crypto2.generateKeyPairSync("ed25519", {
|
|
814
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
815
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
816
|
+
});
|
|
817
|
+
return {
|
|
818
|
+
keyId: hashValue(publicKey).slice(0, 24),
|
|
819
|
+
publicKey,
|
|
820
|
+
privateKey,
|
|
821
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function writeProjectIdentity(projectDir, keypair, licensingDocument) {
|
|
825
|
+
const dir = identityDir(projectDir);
|
|
826
|
+
mkdirSync(dir, { recursive: true });
|
|
827
|
+
writeFileSync(path4.join(dir, ".gitignore"), "*\n", { encoding: "utf8" });
|
|
828
|
+
writeFileSync(keypairPath(projectDir), `${JSON.stringify(keypair, null, 2)}
|
|
829
|
+
`, {
|
|
830
|
+
encoding: "utf8",
|
|
831
|
+
mode: 384
|
|
832
|
+
});
|
|
833
|
+
if (licensingDocument) {
|
|
834
|
+
writeFileSync(path4.join(dir, "licensing.json"), `${JSON.stringify(licensingDocument, null, 2)}
|
|
835
|
+
`, {
|
|
836
|
+
encoding: "utf8",
|
|
837
|
+
mode: 384
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function computeSeatIdentity(projectDir, publicKey, workspaceRoot) {
|
|
842
|
+
const projectPath = path4.resolve(projectDir);
|
|
843
|
+
const workspacePath = workspaceRoot ? path4.resolve(workspaceRoot) : null;
|
|
844
|
+
return {
|
|
845
|
+
publicKey,
|
|
846
|
+
machineHash: hashValue(getMachineFingerprint()),
|
|
847
|
+
appPathHash: hashValue(projectPath),
|
|
848
|
+
workspaceName: workspacePath ? path4.basename(workspacePath) : null,
|
|
849
|
+
workspacePath
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/lib/registry.ts
|
|
854
|
+
import { createWriteStream } from "fs";
|
|
855
|
+
import { mkdir as mkdir2, mkdtemp, rm } from "fs/promises";
|
|
856
|
+
import { tmpdir } from "os";
|
|
857
|
+
import path5 from "path";
|
|
858
|
+
import { pipeline } from "stream/promises";
|
|
859
|
+
import * as tar from "tar";
|
|
860
|
+
async function downloadPackageTarball(input3) {
|
|
861
|
+
const { version, tarballUrl } = await probePackageAccess(input3);
|
|
862
|
+
const tempDir = await mkdtemp(path5.join(tmpdir(), "siteknock-package-download-"));
|
|
863
|
+
const tarballPath = path5.join(tempDir, "package.tgz");
|
|
864
|
+
try {
|
|
865
|
+
await mkdir2(input3.destDir, { recursive: true });
|
|
866
|
+
await downloadTarball({ url: tarballUrl, token: input3.token, destination: tarballPath });
|
|
867
|
+
await tar.x({
|
|
868
|
+
file: tarballPath,
|
|
869
|
+
cwd: input3.destDir,
|
|
870
|
+
strip: 1,
|
|
871
|
+
filter: (entryPath) => entryPath.startsWith("package/")
|
|
872
|
+
});
|
|
873
|
+
return version;
|
|
874
|
+
} finally {
|
|
875
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async function probePackageAccess(input3) {
|
|
879
|
+
const metadata = await fetchRegistryMetadata(input3);
|
|
880
|
+
const version = resolvePackageVersion(metadata, input3.requestedVersion);
|
|
881
|
+
const tarballUrl = metadata.versions?.[version]?.dist?.tarball;
|
|
882
|
+
if (!tarballUrl) {
|
|
883
|
+
throw new Error(`Registry metadata for ${input3.packageName}@${version} does not expose a tarball URL`);
|
|
884
|
+
}
|
|
885
|
+
return { version, tarballUrl };
|
|
886
|
+
}
|
|
887
|
+
async function fetchRegistryMetadata(input3) {
|
|
888
|
+
const registryMetadataUrl = `${input3.registryUrl}/${toRegistryPackagePath(input3.packageName)}`;
|
|
889
|
+
const response = await fetchWithRegistryContext(
|
|
890
|
+
registryMetadataUrl,
|
|
891
|
+
{
|
|
892
|
+
headers: {
|
|
893
|
+
Authorization: toNpmBasicAuth(input3.token),
|
|
894
|
+
Accept: "application/json"
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
"Registry metadata request"
|
|
898
|
+
);
|
|
899
|
+
const body = await response.text();
|
|
900
|
+
if (!response.ok) {
|
|
901
|
+
throw new Error(`Registry metadata request failed with status ${response.status}: ${body}`);
|
|
902
|
+
}
|
|
903
|
+
return JSON.parse(body);
|
|
904
|
+
}
|
|
905
|
+
function resolvePackageVersion(metadata, requestedVersion) {
|
|
906
|
+
if (requestedVersion) {
|
|
907
|
+
if (!metadata.versions?.[requestedVersion]) {
|
|
908
|
+
throw new Error(`Requested version ${requestedVersion} is not available for this token`);
|
|
909
|
+
}
|
|
910
|
+
return requestedVersion;
|
|
911
|
+
}
|
|
912
|
+
const latest = metadata["dist-tags"]?.latest;
|
|
913
|
+
if (latest && metadata.versions?.[latest]) {
|
|
914
|
+
return latest;
|
|
915
|
+
}
|
|
916
|
+
const versions = Object.keys(metadata.versions ?? {}).sort(versionCompare);
|
|
917
|
+
const fallback = versions.at(-1);
|
|
918
|
+
if (!fallback) {
|
|
919
|
+
throw new Error(`Registry returned no visible versions for ${metadata.name}`);
|
|
920
|
+
}
|
|
921
|
+
return fallback;
|
|
922
|
+
}
|
|
923
|
+
async function downloadTarball(input3) {
|
|
924
|
+
const response = await fetchWithRegistryContext(
|
|
925
|
+
input3.url,
|
|
926
|
+
{
|
|
927
|
+
headers: {
|
|
928
|
+
Authorization: toNpmBasicAuth(input3.token)
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
"Registry tarball request"
|
|
932
|
+
);
|
|
933
|
+
if (!response.ok || !response.body) {
|
|
934
|
+
const body = await response.text();
|
|
935
|
+
throw new Error(`Registry tarball request failed with status ${response.status}: ${body}`);
|
|
936
|
+
}
|
|
937
|
+
await pipeline(response.body, createWriteStream(input3.destination));
|
|
938
|
+
}
|
|
939
|
+
function versionCompare(left, right) {
|
|
940
|
+
return new Intl.Collator(void 0, { numeric: true, sensitivity: "base" }).compare(left, right);
|
|
941
|
+
}
|
|
942
|
+
function toRegistryPackagePath(packageName) {
|
|
943
|
+
const [scope, name] = packageName.split("/");
|
|
944
|
+
if (!scope || !name) {
|
|
945
|
+
throw new Error(`Expected scoped package name, received ${packageName}`);
|
|
946
|
+
}
|
|
947
|
+
return `${scope}%2f${name}`;
|
|
948
|
+
}
|
|
949
|
+
function toNpmBasicAuth(token) {
|
|
950
|
+
return `Basic ${Buffer.from(`npm:${token}`).toString("base64")}`;
|
|
951
|
+
}
|
|
952
|
+
async function fetchWithRegistryContext(url, init, label) {
|
|
953
|
+
try {
|
|
954
|
+
return await fetch(url, init);
|
|
955
|
+
} catch (error) {
|
|
956
|
+
throw new Error(`${label} failed for ${url}: ${describeFetchError(error)}`, {
|
|
957
|
+
cause: error instanceof Error ? error : void 0
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function describeFetchError(error) {
|
|
962
|
+
if (!(error instanceof Error)) {
|
|
963
|
+
return String(error);
|
|
964
|
+
}
|
|
965
|
+
const parts = [error.message];
|
|
966
|
+
const cause = error.cause;
|
|
967
|
+
if (cause && typeof cause === "object") {
|
|
968
|
+
const causeRecord = cause;
|
|
969
|
+
const code = typeof causeRecord.code === "string" ? causeRecord.code : void 0;
|
|
970
|
+
const syscall = typeof causeRecord.syscall === "string" ? causeRecord.syscall : void 0;
|
|
971
|
+
const hostname = typeof causeRecord.hostname === "string" ? causeRecord.hostname : void 0;
|
|
972
|
+
if (code) {
|
|
973
|
+
parts.push(`code=${code}`);
|
|
974
|
+
}
|
|
975
|
+
if (hostname) {
|
|
976
|
+
parts.push(`hostname=${hostname}`);
|
|
977
|
+
}
|
|
978
|
+
if (syscall) {
|
|
979
|
+
parts.push(`syscall=${syscall}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return parts.join(", ");
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/lib/project-files.ts
|
|
986
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
987
|
+
import path6 from "path";
|
|
988
|
+
async function bootstrapProjectFiles(input3) {
|
|
989
|
+
await ensureGitignoreEntries(input3.projectDir, [".npmrc"]);
|
|
990
|
+
await writeFile2(path6.join(input3.projectDir, ".npmrc"), createNpmrc(input3.registryUrl, input3.token), "utf8");
|
|
991
|
+
}
|
|
992
|
+
async function ensureGitignoreEntries(projectDir, entries) {
|
|
993
|
+
const gitignorePath = path6.join(projectDir, ".gitignore");
|
|
994
|
+
let content = "";
|
|
995
|
+
try {
|
|
996
|
+
content = await readFile2(gitignorePath, "utf8");
|
|
997
|
+
} catch {
|
|
998
|
+
content = "";
|
|
999
|
+
}
|
|
1000
|
+
const existing = new Set(content.split(/\r?\n/).filter(Boolean));
|
|
1001
|
+
let changed = false;
|
|
1002
|
+
for (const entry of entries) {
|
|
1003
|
+
if (!existing.has(entry)) {
|
|
1004
|
+
content += `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${entry}
|
|
1005
|
+
`;
|
|
1006
|
+
changed = true;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (changed) {
|
|
1010
|
+
await writeFile2(gitignorePath, content, "utf8");
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function createNpmrc(registryUrl, token) {
|
|
1014
|
+
const parsed = new URL(registryUrl);
|
|
1015
|
+
const normalizedPath = parsed.pathname === "/" ? "/" : `${parsed.pathname.replace(/\/$/, "")}/`;
|
|
1016
|
+
return [`@siteknock:registry=${registryUrl}`, `//${parsed.host}${normalizedPath}:_authToken=${token}`, ""].join("\n");
|
|
1017
|
+
}
|
|
1018
|
+
function folderNameToSlug(folderName) {
|
|
1019
|
+
const slug = folderName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
1020
|
+
if (!slug) return "app";
|
|
1021
|
+
return /^[a-z]/.test(slug) ? slug : `app-${slug}`;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/commands/create.ts
|
|
1025
|
+
var createFlags = {
|
|
1026
|
+
project: { type: "string", short: "p" },
|
|
1027
|
+
version: { type: "string" },
|
|
1028
|
+
yes: { type: "boolean" }
|
|
1029
|
+
};
|
|
1030
|
+
var requestedVersionSchema = z4.string().refine((value) => semver.valid(value), "Version must be valid semver");
|
|
1031
|
+
async function runCreateCommand(argv) {
|
|
1032
|
+
const parsed = parseArgs2({
|
|
1033
|
+
args: argv.slice(1),
|
|
1034
|
+
options: createFlags,
|
|
1035
|
+
allowPositionals: true
|
|
1036
|
+
});
|
|
1037
|
+
const config = await getConfig({
|
|
1038
|
+
projectDir: parsed.values.project,
|
|
1039
|
+
requestedVersion: parsed.values.version,
|
|
1040
|
+
yes: parsed.values.yes === true
|
|
1041
|
+
});
|
|
1042
|
+
const resolved = await promptForMissingFields(config);
|
|
1043
|
+
const targetState = await validateTargetFolder(resolved.projectDir);
|
|
1044
|
+
const projectName = getProjectNameFromDirectory(resolved.workspaceRoot, resolved.projectDir);
|
|
1045
|
+
if (resolved.requestedVersion) {
|
|
1046
|
+
requestedVersionSchema.parse(resolved.requestedVersion);
|
|
1047
|
+
}
|
|
1048
|
+
const keypair = generateProjectKeypair();
|
|
1049
|
+
const seatIdentity = computeSeatIdentity(resolved.projectDir, keypair.publicKey, resolved.workspaceRoot);
|
|
1050
|
+
console.log(`Starting browser authorization against ${resolved.licenseUrl} ...`);
|
|
1051
|
+
const license = await authorizeLicenseWithBrowser({
|
|
1052
|
+
licenseUrl: resolved.licenseUrl,
|
|
1053
|
+
requestedVersion: resolved.requestedVersion,
|
|
1054
|
+
purpose: "create",
|
|
1055
|
+
publicKey: seatIdentity.publicKey,
|
|
1056
|
+
machineHash: seatIdentity.machineHash,
|
|
1057
|
+
appPathHash: seatIdentity.appPathHash,
|
|
1058
|
+
projectName,
|
|
1059
|
+
workspaceName: seatIdentity.workspaceName,
|
|
1060
|
+
workspacePath: seatIdentity.workspacePath
|
|
1061
|
+
});
|
|
1062
|
+
validateRequestedVersion(resolved.requestedVersion, license);
|
|
1063
|
+
const registryUrl = license.registryUrl ?? resolved.registryUrl;
|
|
1064
|
+
const generatorPackage = license.generatorPackage ?? resolved.generatorPackage;
|
|
1065
|
+
const projectSlug = folderNameToSlug(projectName);
|
|
1066
|
+
console.log(`Scaffolding project from ${generatorPackage} (registry ${registryUrl}) ...`);
|
|
1067
|
+
let generatorVersion = "";
|
|
1068
|
+
try {
|
|
1069
|
+
generatorVersion = await scaffoldProjectFromGenerator({
|
|
1070
|
+
registryUrl,
|
|
1071
|
+
generatorPackage,
|
|
1072
|
+
token: license.token,
|
|
1073
|
+
requestedVersion: resolved.requestedVersion || void 0,
|
|
1074
|
+
projectDir: resolved.projectDir,
|
|
1075
|
+
slug: projectSlug,
|
|
1076
|
+
name: projectName
|
|
1077
|
+
});
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
if (targetState.createdDirectory) {
|
|
1080
|
+
await rm2(resolved.projectDir, { recursive: true, force: true });
|
|
1081
|
+
}
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
1084
|
+
await bootstrapProjectFiles({
|
|
1085
|
+
projectDir: resolved.projectDir,
|
|
1086
|
+
token: license.token,
|
|
1087
|
+
registryUrl
|
|
1088
|
+
});
|
|
1089
|
+
writeProjectIdentity(resolved.projectDir, keypair, license.licensingDocument);
|
|
1090
|
+
const manifest = await readWorkspaceManifest(resolved.workspaceRoot);
|
|
1091
|
+
const registeredManifest = withRegisteredProject(manifest, {
|
|
1092
|
+
projectName,
|
|
1093
|
+
bundleVersion: generatorVersion,
|
|
1094
|
+
activation: {
|
|
1095
|
+
token: license.token,
|
|
1096
|
+
customerName: license.customerName,
|
|
1097
|
+
plan: license.plan,
|
|
1098
|
+
minVersion: license.minVersion,
|
|
1099
|
+
maxVersion: license.maxVersion,
|
|
1100
|
+
projectSeats: license.projectSeats,
|
|
1101
|
+
expiresAt: license.expiresAt
|
|
1102
|
+
},
|
|
1103
|
+
selected: true
|
|
1104
|
+
});
|
|
1105
|
+
const nextManifest = withStudioSettings(registeredManifest, {
|
|
1106
|
+
image: license.studioImage,
|
|
1107
|
+
appInstallUrl: license.studioAppInstallUrl ?? void 0
|
|
1108
|
+
});
|
|
1109
|
+
await writeWorkspaceManifest(resolved.workspaceRoot, nextManifest);
|
|
1110
|
+
console.log(`Project created at ${resolved.projectDir}`);
|
|
1111
|
+
console.log(`Registry token written to ${path7.join(resolved.projectDir, ".npmrc")}`);
|
|
1112
|
+
console.log(`Workspace config updated at ${getWorkspaceConfigPath(resolved.workspaceRoot)}`);
|
|
1113
|
+
console.log(`Scaffolded from ${generatorPackage}@${generatorVersion}`);
|
|
1114
|
+
console.log(`Entitlement verified for ${license.plan} (${license.minVersion} -> ${license.maxVersion})`);
|
|
1115
|
+
if (typeof license.seatIndex === "number") {
|
|
1116
|
+
console.log(`Project seat #${license.seatIndex} activated for ${projectName}`);
|
|
1117
|
+
}
|
|
1118
|
+
console.log(`Next: cd ${path7.basename(resolved.projectDir)} && pnpm install && pnpm app create <name>`);
|
|
1119
|
+
}
|
|
1120
|
+
async function scaffoldProjectFromGenerator(input3) {
|
|
1121
|
+
const generatorDir = await mkdtemp2(path7.join(tmpdir2(), "siteknock-generator-"));
|
|
1122
|
+
try {
|
|
1123
|
+
const version = await downloadPackageTarball({
|
|
1124
|
+
registryUrl: input3.registryUrl,
|
|
1125
|
+
packageName: input3.generatorPackage,
|
|
1126
|
+
token: input3.token,
|
|
1127
|
+
requestedVersion: input3.requestedVersion,
|
|
1128
|
+
destDir: generatorDir
|
|
1129
|
+
});
|
|
1130
|
+
await runNode([
|
|
1131
|
+
path7.join(generatorDir, "project-cli.mjs"),
|
|
1132
|
+
"init",
|
|
1133
|
+
"--slug",
|
|
1134
|
+
input3.slug,
|
|
1135
|
+
"--name",
|
|
1136
|
+
input3.name,
|
|
1137
|
+
"--version",
|
|
1138
|
+
version,
|
|
1139
|
+
"--dir",
|
|
1140
|
+
input3.projectDir,
|
|
1141
|
+
"--dep-mode",
|
|
1142
|
+
"pinned"
|
|
1143
|
+
]);
|
|
1144
|
+
return version;
|
|
1145
|
+
} finally {
|
|
1146
|
+
await rm2(generatorDir, { recursive: true, force: true });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function runNode(args) {
|
|
1150
|
+
return new Promise((resolve, reject) => {
|
|
1151
|
+
const child = spawn2(process.execPath, args, { stdio: "inherit" });
|
|
1152
|
+
child.on("error", reject);
|
|
1153
|
+
child.on("exit", (code) => {
|
|
1154
|
+
if (code === 0) {
|
|
1155
|
+
resolve();
|
|
1156
|
+
} else {
|
|
1157
|
+
reject(new Error(`sk-project init exited with code ${code ?? "unknown"}`));
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
async function validateTargetFolder(projectDir) {
|
|
1163
|
+
const exists = await statSafe(projectDir);
|
|
1164
|
+
if (!exists) {
|
|
1165
|
+
await mkdir3(path7.dirname(projectDir), { recursive: true });
|
|
1166
|
+
return { createdDirectory: true };
|
|
1167
|
+
}
|
|
1168
|
+
if (!await targetDirectoryExistsAndIsNonEmpty(projectDir)) {
|
|
1169
|
+
return { createdDirectory: false };
|
|
1170
|
+
}
|
|
1171
|
+
throw new Error(`Target project directory already exists and is not empty: ${projectDir}`);
|
|
1172
|
+
}
|
|
1173
|
+
async function statSafe(targetPath) {
|
|
1174
|
+
try {
|
|
1175
|
+
await stat(targetPath);
|
|
1176
|
+
return true;
|
|
1177
|
+
} catch {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
async function targetDirectoryExistsAndIsNonEmpty(projectDir) {
|
|
1182
|
+
try {
|
|
1183
|
+
const entries = await (await import("fs/promises")).readdir(projectDir);
|
|
1184
|
+
return entries.length > 0;
|
|
1185
|
+
} catch {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function validateRequestedVersion(version, license) {
|
|
1190
|
+
if (!version) return;
|
|
1191
|
+
const minVersion = semver.valid(license.minVersion);
|
|
1192
|
+
const maxVersion = semver.valid(license.maxVersion);
|
|
1193
|
+
if (!minVersion || !maxVersion) {
|
|
1194
|
+
throw new Error("Licensing server returned an invalid version range");
|
|
1195
|
+
}
|
|
1196
|
+
if (semver.lt(version, minVersion) || semver.gt(version, maxVersion)) {
|
|
1197
|
+
throw new Error(`Requested version ${version} is outside the licensed range ${minVersion} -> ${maxVersion}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/commands/locale.ts
|
|
1202
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1203
|
+
import { spawn as spawn3 } from "child_process";
|
|
1204
|
+
import path8 from "path";
|
|
1205
|
+
import process4 from "process";
|
|
1206
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
1207
|
+
var localeFlags = {
|
|
1208
|
+
project: { type: "string", short: "p" }
|
|
1209
|
+
};
|
|
1210
|
+
async function runLocaleCommand(argv) {
|
|
1211
|
+
const subArgs = argv.slice(1);
|
|
1212
|
+
const subcommand = subArgs.find((value) => !value.startsWith("-"));
|
|
1213
|
+
if (!subcommand || subcommand === "help") {
|
|
1214
|
+
printLocaleHelp();
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const parsed = parseArgs3({ args: subArgs, options: localeFlags, allowPositionals: true, strict: false });
|
|
1218
|
+
const projectDir = await resolveProjectDir2(parsed.values.project);
|
|
1219
|
+
const localeCliPath = path8.join(projectDir, "node_modules", "@siteknock", "generator", "locale-cli.mjs");
|
|
1220
|
+
if (!existsSync4(localeCliPath)) {
|
|
1221
|
+
throw new Error(`Could not find @siteknock/generator in ${projectDir}. Run install in the project first.`);
|
|
1222
|
+
}
|
|
1223
|
+
const forwarded = stripProjectFlag2(subArgs);
|
|
1224
|
+
await delegate2(localeCliPath, projectDir, forwarded);
|
|
1225
|
+
}
|
|
1226
|
+
async function resolveProjectDir2(projectFlag) {
|
|
1227
|
+
const workspaceRoot = await resolveWorkspaceRoot(process4.cwd());
|
|
1228
|
+
if (projectFlag) {
|
|
1229
|
+
return resolveTopLevelProjectDirectory(workspaceRoot, projectFlag).projectDir;
|
|
1230
|
+
}
|
|
1231
|
+
if (existsSync4(path8.join(process4.cwd(), "config", "sk-config.jsonc"))) {
|
|
1232
|
+
return process4.cwd();
|
|
1233
|
+
}
|
|
1234
|
+
const manifest = await readWorkspaceManifest(workspaceRoot);
|
|
1235
|
+
const defaultProject = manifest.studio.defaultProject;
|
|
1236
|
+
if (defaultProject) {
|
|
1237
|
+
return resolveTopLevelProjectDirectory(workspaceRoot, defaultProject).projectDir;
|
|
1238
|
+
}
|
|
1239
|
+
throw new Error("No project specified. Pass --project <folder> or run inside a project directory.");
|
|
1240
|
+
}
|
|
1241
|
+
function stripProjectFlag2(args) {
|
|
1242
|
+
const out = [];
|
|
1243
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1244
|
+
const arg = args[i];
|
|
1245
|
+
if (arg === "--project" || arg === "-p") {
|
|
1246
|
+
i += 1;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (arg.startsWith("--project=")) continue;
|
|
1250
|
+
out.push(arg);
|
|
1251
|
+
}
|
|
1252
|
+
return out;
|
|
1253
|
+
}
|
|
1254
|
+
function delegate2(localeCliPath, projectDir, args) {
|
|
1255
|
+
return new Promise((resolve, reject) => {
|
|
1256
|
+
const child = spawn3("node", [localeCliPath, ...args], { cwd: projectDir, stdio: "inherit" });
|
|
1257
|
+
child.on("error", reject);
|
|
1258
|
+
child.on("exit", (code) => {
|
|
1259
|
+
if (code === 0) resolve();
|
|
1260
|
+
else reject(new Error(`locale command exited with code ${code ?? "null"}`));
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
function printLocaleHelp() {
|
|
1265
|
+
console.log(`
|
|
1266
|
+
siteknock locale \u2014 manage per-app i18n languages within a project
|
|
1267
|
+
|
|
1268
|
+
Usage:
|
|
1269
|
+
siteknock locale list --project <folder>
|
|
1270
|
+
siteknock locale add <code> --project <folder> --app <name>
|
|
1271
|
+
|
|
1272
|
+
Notes:
|
|
1273
|
+
--app selects which app to add the language to (defaults to the first app).
|
|
1274
|
+
Adding a language seeds locales/<app>/<code>/ from the kit catalog.
|
|
1275
|
+
`);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/commands/probe.ts
|
|
1279
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
1280
|
+
import semver2 from "semver";
|
|
1281
|
+
import { z as z5 } from "zod";
|
|
1282
|
+
var probeFlags = {
|
|
1283
|
+
version: { type: "string" },
|
|
1284
|
+
yes: { type: "boolean" }
|
|
1285
|
+
};
|
|
1286
|
+
var requestedVersionSchema2 = z5.string().refine((value) => semver2.valid(value), "Version must be valid semver");
|
|
1287
|
+
async function runProbeCommand(argv) {
|
|
1288
|
+
const parsed = parseArgs4({
|
|
1289
|
+
args: argv.slice(1),
|
|
1290
|
+
options: probeFlags,
|
|
1291
|
+
allowPositionals: true
|
|
1292
|
+
});
|
|
1293
|
+
const config = await getConfig({
|
|
1294
|
+
requestedVersion: parsed.values.version,
|
|
1295
|
+
yes: parsed.values.yes === true
|
|
1296
|
+
});
|
|
1297
|
+
const resolved = await promptForMissingFieldsWithOptions(config, { skipProjectDirPrompt: true });
|
|
1298
|
+
if (resolved.requestedVersion) {
|
|
1299
|
+
requestedVersionSchema2.parse(resolved.requestedVersion);
|
|
1300
|
+
}
|
|
1301
|
+
console.log(`Starting browser authorization against ${resolved.licenseUrl} ...`);
|
|
1302
|
+
const license = await authorizeLicenseWithBrowser({
|
|
1303
|
+
licenseUrl: resolved.licenseUrl,
|
|
1304
|
+
requestedVersion: resolved.requestedVersion,
|
|
1305
|
+
purpose: "probe"
|
|
1306
|
+
});
|
|
1307
|
+
validateRequestedVersion2(resolved.requestedVersion, license.minVersion, license.maxVersion);
|
|
1308
|
+
const registryUrl = license.registryUrl ?? resolved.registryUrl;
|
|
1309
|
+
const generatorPackage = resolved.generatorPackage;
|
|
1310
|
+
console.log(`Checking registry visibility at ${registryUrl} ...`);
|
|
1311
|
+
const probe = await probePackageAccess({
|
|
1312
|
+
registryUrl,
|
|
1313
|
+
packageName: generatorPackage,
|
|
1314
|
+
token: license.token,
|
|
1315
|
+
requestedVersion: resolved.requestedVersion || void 0
|
|
1316
|
+
});
|
|
1317
|
+
console.log("Probe succeeded");
|
|
1318
|
+
console.log(`Customer: ${license.customerName}`);
|
|
1319
|
+
console.log(`Plan: ${license.plan}`);
|
|
1320
|
+
console.log(`Version range: ${license.minVersion} -> ${license.maxVersion}`);
|
|
1321
|
+
console.log(`Visible package: ${generatorPackage}@${probe.version}`);
|
|
1322
|
+
}
|
|
1323
|
+
function validateRequestedVersion2(version, minVersionRaw, maxVersionRaw) {
|
|
1324
|
+
if (!version) return;
|
|
1325
|
+
const minVersion = semver2.valid(minVersionRaw);
|
|
1326
|
+
const maxVersion = semver2.valid(maxVersionRaw);
|
|
1327
|
+
if (!minVersion || !maxVersion) {
|
|
1328
|
+
throw new Error("Licensing server returned an invalid version range");
|
|
1329
|
+
}
|
|
1330
|
+
if (semver2.lt(version, minVersion) || semver2.gt(version, maxVersion)) {
|
|
1331
|
+
throw new Error(`Requested version ${version} is outside the licensed range ${minVersion} -> ${maxVersion}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/commands/studio.ts
|
|
1336
|
+
import { createHash as createHash2 } from "crypto";
|
|
1337
|
+
import { execFile, spawn as spawn4 } from "child_process";
|
|
1338
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1339
|
+
import net from "net";
|
|
1340
|
+
import os2 from "os";
|
|
1341
|
+
import path9 from "path";
|
|
1342
|
+
import process5 from "process";
|
|
1343
|
+
import readline2 from "readline/promises";
|
|
1344
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
1345
|
+
import { promisify } from "util";
|
|
1346
|
+
import { stdin as input2, stdout as output2 } from "process";
|
|
1347
|
+
var execFileAsync = promisify(execFile);
|
|
1348
|
+
var DEFAULT_STUDIO_IMAGE = "ghcr.io/siteknock/sk-studio";
|
|
1349
|
+
var DEV_STUDIO_IMAGE_ENV = "SK_DEV_STUDIO_IMAGE";
|
|
1350
|
+
var DEFAULT_STUDIO_PORT = 4875;
|
|
1351
|
+
var DEFAULT_HEALTH_TIMEOUT_MS = 6e4;
|
|
1352
|
+
var APP_HEALTH_PATH = "/api/health";
|
|
1353
|
+
var IS_TTY = process5.stdout.isTTY === true;
|
|
1354
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1355
|
+
var Spinner = class {
|
|
1356
|
+
frame = 0;
|
|
1357
|
+
timer = null;
|
|
1358
|
+
label = "";
|
|
1359
|
+
startedAt = 0;
|
|
1360
|
+
start(label) {
|
|
1361
|
+
this.label = label;
|
|
1362
|
+
this.startedAt = Date.now();
|
|
1363
|
+
this.frame = 0;
|
|
1364
|
+
if (!IS_TTY) {
|
|
1365
|
+
process5.stdout.write(`${label}...
|
|
1366
|
+
`);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
this.timer = setInterval(() => this.tick(), 80);
|
|
1370
|
+
this.tick();
|
|
1371
|
+
}
|
|
1372
|
+
succeed(label) {
|
|
1373
|
+
const text = label ?? this.label;
|
|
1374
|
+
this.clear();
|
|
1375
|
+
process5.stdout.write(`\x1B[32m\u2713\x1B[0m ${text}
|
|
1376
|
+
`);
|
|
1377
|
+
}
|
|
1378
|
+
fail(label) {
|
|
1379
|
+
const text = label ?? this.label;
|
|
1380
|
+
this.clear();
|
|
1381
|
+
process5.stdout.write(`\x1B[31m\u2717\x1B[0m ${text}
|
|
1382
|
+
`);
|
|
1383
|
+
}
|
|
1384
|
+
stop() {
|
|
1385
|
+
this.clear();
|
|
1386
|
+
}
|
|
1387
|
+
tick() {
|
|
1388
|
+
const elapsed = Math.floor((Date.now() - this.startedAt) / 1e3);
|
|
1389
|
+
const suffix = elapsed > 0 ? ` (${elapsed}s)` : "";
|
|
1390
|
+
const icon = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
1391
|
+
this.frame++;
|
|
1392
|
+
const line = `${icon} ${this.label}${suffix} `;
|
|
1393
|
+
process5.stdout.write(`\r${line}`);
|
|
1394
|
+
}
|
|
1395
|
+
clear() {
|
|
1396
|
+
if (this.timer) {
|
|
1397
|
+
clearInterval(this.timer);
|
|
1398
|
+
this.timer = null;
|
|
1399
|
+
}
|
|
1400
|
+
if (IS_TTY) {
|
|
1401
|
+
const width = process5.stdout.columns || 80;
|
|
1402
|
+
process5.stdout.write("\r" + " ".repeat(width) + "\r");
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
async function pullImage(image) {
|
|
1407
|
+
if (IS_TTY) {
|
|
1408
|
+
process5.stdout.write(`\x1B[2mPulling image: ${image}\x1B[0m
|
|
1409
|
+
`);
|
|
1410
|
+
} else {
|
|
1411
|
+
process5.stdout.write(`Pulling image: ${image}
|
|
1412
|
+
`);
|
|
1413
|
+
}
|
|
1414
|
+
await new Promise((resolve, reject) => {
|
|
1415
|
+
const child = spawn4("docker", ["pull", image], { stdio: "inherit" });
|
|
1416
|
+
child.on("error", reject);
|
|
1417
|
+
child.on("exit", (code) => {
|
|
1418
|
+
if (code === 0) resolve();
|
|
1419
|
+
else reject(new Error(`docker pull exited with code ${code}`));
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
var STUDIO_APP_INSTALL_URL = process5.env.SK_STUDIO_APP_INSTALL_URL?.trim() || null;
|
|
1424
|
+
var studioFlags = {
|
|
1425
|
+
project: { type: "string", short: "p" },
|
|
1426
|
+
mode: { type: "string", short: "m" },
|
|
1427
|
+
port: { type: "string" },
|
|
1428
|
+
image: { type: "string" },
|
|
1429
|
+
"container-name": { type: "string" },
|
|
1430
|
+
"no-open": { type: "boolean" }
|
|
1431
|
+
};
|
|
1432
|
+
async function runStudioCommand(argv) {
|
|
1433
|
+
const parsed = parseArgs5({
|
|
1434
|
+
args: argv.slice(1),
|
|
1435
|
+
options: studioFlags,
|
|
1436
|
+
allowPositionals: true
|
|
1437
|
+
});
|
|
1438
|
+
const action = normalizeAction(parsed.positionals[0]);
|
|
1439
|
+
const config = await getConfig({ yes: true });
|
|
1440
|
+
const manifest = await readWorkspaceManifest(config.workspaceRoot);
|
|
1441
|
+
const registeredProjects = listWorkspaceProjects(manifest);
|
|
1442
|
+
const targetProject = resolveStudioProject(manifest, parsed.values.project);
|
|
1443
|
+
const noOpen = parsed.values["no-open"] === true;
|
|
1444
|
+
const mode = resolveStudioMode(parsed.values.mode, manifest.studio.mode);
|
|
1445
|
+
const appInstallUrl = STUDIO_APP_INSTALL_URL ?? manifest.studio.appInstallUrl ?? null;
|
|
1446
|
+
if (mode === "app") {
|
|
1447
|
+
await runStudioAppMode({
|
|
1448
|
+
action,
|
|
1449
|
+
workspaceRoot: config.workspaceRoot,
|
|
1450
|
+
manifest,
|
|
1451
|
+
targetProject,
|
|
1452
|
+
noOpen,
|
|
1453
|
+
appInstallUrl,
|
|
1454
|
+
mode
|
|
1455
|
+
});
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const image = parsed.values.image ?? readHiddenOverride(DEV_STUDIO_IMAGE_ENV) ?? manifest.studio.image ?? DEFAULT_STUDIO_IMAGE;
|
|
1459
|
+
const containerName = parsed.values["container-name"] ?? manifest.studio.containerName ?? buildContainerName(config.workspaceRoot);
|
|
1460
|
+
const requestedPort = parsePort(parsed.values.port);
|
|
1461
|
+
if (action !== "stop" && action !== "status" && registeredProjects.length === 0) {
|
|
1462
|
+
throw new Error(
|
|
1463
|
+
`No projects are registered in ${getWorkspaceConfigPath(config.workspaceRoot)}. Run \`siteknock create --project <folder>\` from ${config.workspaceRoot} first.`
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
await ensureDockerAvailable();
|
|
1467
|
+
if (action === "stop") {
|
|
1468
|
+
await stopContainer(containerName);
|
|
1469
|
+
console.log(`Stopped Studio container ${containerName}`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (action === "status") {
|
|
1473
|
+
const state = await inspectContainer(containerName);
|
|
1474
|
+
if (!state.exists) {
|
|
1475
|
+
console.log(`Studio container ${containerName} is not created`);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
console.log(
|
|
1479
|
+
state.running ? `Studio container ${containerName} is running` : `Studio container ${containerName} is stopped`
|
|
1480
|
+
);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const port = requestedPort ?? manifest.studio.port ?? await findAvailablePort(DEFAULT_STUDIO_PORT);
|
|
1484
|
+
await authenticateToGhcrIfNeeded(image);
|
|
1485
|
+
if (action === "restart") {
|
|
1486
|
+
await recreateContainer({ containerName, image, port, workspaceRoot: config.workspaceRoot });
|
|
1487
|
+
} else {
|
|
1488
|
+
const state = await inspectContainer(containerName);
|
|
1489
|
+
if (!state.exists) {
|
|
1490
|
+
await createContainer({ containerName, image, port, workspaceRoot: config.workspaceRoot });
|
|
1491
|
+
} else if (!state.running) {
|
|
1492
|
+
if (requestedPort || parsed.values.image || parsed.values["container-name"]) {
|
|
1493
|
+
await recreateContainer({ containerName, image, port, workspaceRoot: config.workspaceRoot });
|
|
1494
|
+
} else {
|
|
1495
|
+
const startSpinner = new Spinner();
|
|
1496
|
+
startSpinner.start("Starting Studio container");
|
|
1497
|
+
await runDocker(["start", containerName]);
|
|
1498
|
+
startSpinner.succeed("Container started");
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
await waitForStudio(port);
|
|
1503
|
+
const nextManifest = withStudioSettings(manifest, {
|
|
1504
|
+
defaultProject: targetProject ?? void 0,
|
|
1505
|
+
mode: "docker",
|
|
1506
|
+
image,
|
|
1507
|
+
port,
|
|
1508
|
+
containerName
|
|
1509
|
+
});
|
|
1510
|
+
await writeWorkspaceManifest(config.workspaceRoot, nextManifest);
|
|
1511
|
+
const targetUrl = await buildStudioUrl({
|
|
1512
|
+
projectName: targetProject,
|
|
1513
|
+
port
|
|
1514
|
+
});
|
|
1515
|
+
console.log(`Studio workspace: ${config.workspaceRoot}`);
|
|
1516
|
+
if (targetProject) {
|
|
1517
|
+
console.log(`Selected project: ${targetProject}`);
|
|
1518
|
+
}
|
|
1519
|
+
console.log(`Studio URL: ${targetUrl}`);
|
|
1520
|
+
if (!noOpen) {
|
|
1521
|
+
openBrowser(targetUrl);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function resolveStudioMode(flagValue, persisted) {
|
|
1525
|
+
if (flagValue === "docker" || flagValue === "app") return flagValue;
|
|
1526
|
+
if (flagValue !== void 0) throw new Error(`Invalid --mode value: ${flagValue}. Use "docker" or "app".`);
|
|
1527
|
+
return persisted ?? "docker";
|
|
1528
|
+
}
|
|
1529
|
+
function resolveElectronAppExecutable() {
|
|
1530
|
+
switch (process5.platform) {
|
|
1531
|
+
case "darwin": {
|
|
1532
|
+
const candidates = [
|
|
1533
|
+
"/Applications/SiteKnock Studio.app/Contents/MacOS/SiteKnock Studio",
|
|
1534
|
+
path9.join(os2.homedir(), "Applications", "SiteKnock Studio.app", "Contents", "MacOS", "SiteKnock Studio")
|
|
1535
|
+
];
|
|
1536
|
+
return candidates.find(existsSync5) ?? null;
|
|
1537
|
+
}
|
|
1538
|
+
case "win32": {
|
|
1539
|
+
const candidates = [
|
|
1540
|
+
path9.join(os2.homedir(), "AppData", "Local", "Programs", "SiteKnock Studio", "SiteKnock Studio.exe"),
|
|
1541
|
+
path9.join("C:", "Program Files", "SiteKnock Studio", "SiteKnock Studio.exe")
|
|
1542
|
+
];
|
|
1543
|
+
return candidates.find(existsSync5) ?? null;
|
|
1544
|
+
}
|
|
1545
|
+
default: {
|
|
1546
|
+
const candidates = [
|
|
1547
|
+
path9.join(os2.homedir(), ".local", "bin", "siteknock-studio"),
|
|
1548
|
+
"/usr/bin/siteknock-studio",
|
|
1549
|
+
"/opt/SiteKnock Studio/siteknock-studio"
|
|
1550
|
+
];
|
|
1551
|
+
return candidates.find(existsSync5) ?? null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
function launchElectronApp(executable, workspaceRoot, projectName) {
|
|
1556
|
+
if (process5.platform === "darwin") {
|
|
1557
|
+
const appBundle = executable.replace(/\/Contents\/MacOS\/.*$/, "");
|
|
1558
|
+
const extraArgs2 = ["--workspace", workspaceRoot, ...projectName ? ["--project", projectName] : []];
|
|
1559
|
+
spawn4("open", ["-a", appBundle, "--args", ...extraArgs2], { detached: true, stdio: "ignore" }).unref();
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const extraArgs = ["--workspace", workspaceRoot, ...projectName ? ["--project", projectName] : []];
|
|
1563
|
+
spawn4(executable, extraArgs, { detached: true, stdio: "ignore" }).unref();
|
|
1564
|
+
}
|
|
1565
|
+
async function runStudioAppMode(input3) {
|
|
1566
|
+
const { action, workspaceRoot, manifest, targetProject, noOpen, appInstallUrl, mode } = input3;
|
|
1567
|
+
const executable = resolveElectronAppExecutable();
|
|
1568
|
+
if (action === "status") {
|
|
1569
|
+
if (!executable) {
|
|
1570
|
+
console.log("SiteKnock Studio app is not installed.");
|
|
1571
|
+
} else {
|
|
1572
|
+
console.log(`SiteKnock Studio app found: ${executable}`);
|
|
1573
|
+
}
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (action === "stop") {
|
|
1577
|
+
if (process5.platform === "darwin") {
|
|
1578
|
+
spawn4("osascript", ["-e", 'quit app "SiteKnock Studio"'], { detached: true, stdio: "ignore" }).unref();
|
|
1579
|
+
console.log("Sent quit signal to SiteKnock Studio.");
|
|
1580
|
+
} else {
|
|
1581
|
+
console.log("Use your OS task manager to quit SiteKnock Studio.");
|
|
1582
|
+
}
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (!executable) {
|
|
1586
|
+
console.log("SiteKnock Studio app is not installed.\n");
|
|
1587
|
+
if (appInstallUrl) {
|
|
1588
|
+
console.log(`Install it from:
|
|
1589
|
+
${appInstallUrl}
|
|
1590
|
+
`);
|
|
1591
|
+
} else {
|
|
1592
|
+
console.log("No desktop-app install URL is configured in SiteKnock.");
|
|
1593
|
+
console.log("Desktop distribution should stay private and be wired through an internal release channel.");
|
|
1594
|
+
console.log("For now, use --mode docker or set SK_STUDIO_APP_INSTALL_URL to your internal installer URL.");
|
|
1595
|
+
console.log("A better long-term path is a CLI-managed install/update flow backed by private storage.");
|
|
1596
|
+
console.log("");
|
|
1597
|
+
}
|
|
1598
|
+
console.log("After installation, run this command again.");
|
|
1599
|
+
console.log("\nAlternatively, use --mode docker to run via Docker instead.");
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const nextManifest = withStudioSettings(manifest, { mode, defaultProject: targetProject ?? void 0 });
|
|
1603
|
+
await writeWorkspaceManifest(workspaceRoot, nextManifest);
|
|
1604
|
+
console.log(`Studio workspace: ${workspaceRoot}`);
|
|
1605
|
+
if (targetProject) console.log(`Selected project: ${targetProject}`);
|
|
1606
|
+
console.log(`Launching SiteKnock Studio app...`);
|
|
1607
|
+
if (!noOpen) {
|
|
1608
|
+
launchElectronApp(executable, workspaceRoot, targetProject);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function normalizeAction(rawAction) {
|
|
1612
|
+
const action = rawAction?.trim().toLowerCase() || "start";
|
|
1613
|
+
if (action === "start" || action === "stop" || action === "restart" || action === "status") {
|
|
1614
|
+
return action;
|
|
1615
|
+
}
|
|
1616
|
+
throw new Error(`Unknown studio action: ${rawAction}`);
|
|
1617
|
+
}
|
|
1618
|
+
function resolveStudioProject(manifest, requestedProject) {
|
|
1619
|
+
if (requestedProject) {
|
|
1620
|
+
if (!manifest.projects[requestedProject]) {
|
|
1621
|
+
throw new Error(`Project ${requestedProject} is not registered in ${WORKSPACE_CONFIG_FILENAME}`);
|
|
1622
|
+
}
|
|
1623
|
+
return requestedProject;
|
|
1624
|
+
}
|
|
1625
|
+
return resolvePreferredProject(manifest);
|
|
1626
|
+
}
|
|
1627
|
+
function parsePort(rawPort) {
|
|
1628
|
+
if (!rawPort) {
|
|
1629
|
+
return void 0;
|
|
1630
|
+
}
|
|
1631
|
+
const parsed = Number.parseInt(rawPort, 10);
|
|
1632
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
1633
|
+
throw new Error(`Invalid port: ${rawPort}`);
|
|
1634
|
+
}
|
|
1635
|
+
return parsed;
|
|
1636
|
+
}
|
|
1637
|
+
function buildContainerName(workspaceRoot) {
|
|
1638
|
+
const baseName = path9.basename(workspaceRoot).replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
|
|
1639
|
+
const digest = createHash2("sha1").update(workspaceRoot).digest("hex").slice(0, 8);
|
|
1640
|
+
return `sk-studio-${baseName}-${digest}`;
|
|
1641
|
+
}
|
|
1642
|
+
async function ensureDockerAvailable() {
|
|
1643
|
+
try {
|
|
1644
|
+
await runDocker(["info"]);
|
|
1645
|
+
} catch {
|
|
1646
|
+
throw new Error("Docker is not running. Start Docker Desktop or OrbStack and retry.");
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
async function authenticateToGhcrIfNeeded(image) {
|
|
1650
|
+
const token = process5.env.GHCR_TOKEN ?? process5.env.GITHUB_TOKEN;
|
|
1651
|
+
if (!token || !image.startsWith("ghcr.io/")) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const username = await resolveGhcrUsername();
|
|
1655
|
+
const authSpinner = new Spinner();
|
|
1656
|
+
authSpinner.start("Authenticating to ghcr.io");
|
|
1657
|
+
await new Promise((resolve, reject) => {
|
|
1658
|
+
const child = spawn4("docker", ["login", "ghcr.io", "-u", username, "--password-stdin"], {
|
|
1659
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1660
|
+
});
|
|
1661
|
+
let stderr = "";
|
|
1662
|
+
child.stderr.on("data", (chunk) => {
|
|
1663
|
+
stderr += chunk.toString();
|
|
1664
|
+
});
|
|
1665
|
+
child.on("error", (err) => {
|
|
1666
|
+
authSpinner.fail("Authentication failed");
|
|
1667
|
+
reject(err);
|
|
1668
|
+
});
|
|
1669
|
+
child.on("exit", (code) => {
|
|
1670
|
+
if (code === 0) {
|
|
1671
|
+
authSpinner.succeed("Authenticated to ghcr.io");
|
|
1672
|
+
resolve();
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
authSpinner.fail("Authentication failed");
|
|
1676
|
+
reject(new Error(stderr.trim() || "Docker login failed"));
|
|
1677
|
+
});
|
|
1678
|
+
child.stdin.end(token);
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
async function resolveGhcrUsername() {
|
|
1682
|
+
const envValue = process5.env.GHCR_USERNAME ?? process5.env.GH_USER ?? process5.env.GITHUB_ACTOR;
|
|
1683
|
+
if (envValue) {
|
|
1684
|
+
return envValue;
|
|
1685
|
+
}
|
|
1686
|
+
try {
|
|
1687
|
+
const { stdout } = await execFileAsync("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8" });
|
|
1688
|
+
const ghUser = stdout.trim();
|
|
1689
|
+
if (ghUser) {
|
|
1690
|
+
return ghUser;
|
|
1691
|
+
}
|
|
1692
|
+
} catch {
|
|
1693
|
+
}
|
|
1694
|
+
const rl = readline2.createInterface({ input: input2, output: output2 });
|
|
1695
|
+
try {
|
|
1696
|
+
const reply = (await rl.question("GitHub username for ghcr.io login: ")).trim();
|
|
1697
|
+
if (!reply) {
|
|
1698
|
+
throw new Error("Missing GitHub username for ghcr.io login");
|
|
1699
|
+
}
|
|
1700
|
+
return reply;
|
|
1701
|
+
} finally {
|
|
1702
|
+
rl.close();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
async function inspectContainer(containerName) {
|
|
1706
|
+
try {
|
|
1707
|
+
const { stdout } = await execFileAsync("docker", ["inspect", containerName, "--format", "{{json .State}}"], {
|
|
1708
|
+
encoding: "utf8"
|
|
1709
|
+
});
|
|
1710
|
+
const state = JSON.parse(stdout.trim());
|
|
1711
|
+
return {
|
|
1712
|
+
exists: true,
|
|
1713
|
+
running: state.Running === true
|
|
1714
|
+
};
|
|
1715
|
+
} catch {
|
|
1716
|
+
return {
|
|
1717
|
+
exists: false,
|
|
1718
|
+
running: false
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
async function createContainer(input3) {
|
|
1723
|
+
await pullImage(input3.image);
|
|
1724
|
+
const spinner = new Spinner();
|
|
1725
|
+
spinner.start("Creating Studio container");
|
|
1726
|
+
try {
|
|
1727
|
+
await runDocker([
|
|
1728
|
+
"run",
|
|
1729
|
+
"-d",
|
|
1730
|
+
"--name",
|
|
1731
|
+
input3.containerName,
|
|
1732
|
+
"--init",
|
|
1733
|
+
"--read-only",
|
|
1734
|
+
"--restart",
|
|
1735
|
+
"unless-stopped",
|
|
1736
|
+
"--tmpfs",
|
|
1737
|
+
"/tmp:size=64m,mode=1777",
|
|
1738
|
+
"--tmpfs",
|
|
1739
|
+
"/home/nonroot:size=16m,mode=0700",
|
|
1740
|
+
"--security-opt",
|
|
1741
|
+
"no-new-privileges:true",
|
|
1742
|
+
"--cap-drop",
|
|
1743
|
+
"ALL",
|
|
1744
|
+
"--label",
|
|
1745
|
+
`com.siteknock.sk-cli.workspace=${input3.workspaceRoot}`,
|
|
1746
|
+
"--label",
|
|
1747
|
+
`com.siteknock.sk-cli.port=${input3.port}`,
|
|
1748
|
+
"--label",
|
|
1749
|
+
`com.siteknock.sk-cli.image=${input3.image}`,
|
|
1750
|
+
"--health-cmd",
|
|
1751
|
+
`/nodejs/bin/node -e "fetch('http://127.0.0.1:5100/api/health').then((response)=>process.exit(response.ok?0:1)).catch(()=>process.exit(1))"`,
|
|
1752
|
+
"--health-interval",
|
|
1753
|
+
"30s",
|
|
1754
|
+
"--health-timeout",
|
|
1755
|
+
"5s",
|
|
1756
|
+
"--health-start-period",
|
|
1757
|
+
"20s",
|
|
1758
|
+
"--health-retries",
|
|
1759
|
+
"3",
|
|
1760
|
+
"-p",
|
|
1761
|
+
`${input3.port}:3100`,
|
|
1762
|
+
"-e",
|
|
1763
|
+
"WORKSPACE_DIR=/workspace",
|
|
1764
|
+
"-e",
|
|
1765
|
+
`STUDIO_INSTANCE_ID=${os2.hostname()}-${path9.basename(input3.workspaceRoot)}`,
|
|
1766
|
+
"-v",
|
|
1767
|
+
`${input3.workspaceRoot}:/workspace`,
|
|
1768
|
+
input3.image
|
|
1769
|
+
]);
|
|
1770
|
+
spinner.succeed("Studio container created");
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
spinner.fail("Failed to create Studio container");
|
|
1773
|
+
throw err;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
async function recreateContainer(input3) {
|
|
1777
|
+
const stopSpinner = new Spinner();
|
|
1778
|
+
stopSpinner.start("Stopping container");
|
|
1779
|
+
await stopContainer(input3.containerName);
|
|
1780
|
+
stopSpinner.succeed("Container stopped");
|
|
1781
|
+
const removeSpinner = new Spinner();
|
|
1782
|
+
removeSpinner.start("Removing container");
|
|
1783
|
+
await removeContainer(input3.containerName);
|
|
1784
|
+
removeSpinner.succeed("Container removed");
|
|
1785
|
+
await createContainer(input3);
|
|
1786
|
+
}
|
|
1787
|
+
async function stopContainer(containerName) {
|
|
1788
|
+
const state = await inspectContainer(containerName);
|
|
1789
|
+
if (!state.exists || !state.running) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
await runDocker(["stop", containerName]);
|
|
1793
|
+
}
|
|
1794
|
+
async function removeContainer(containerName) {
|
|
1795
|
+
const state = await inspectContainer(containerName);
|
|
1796
|
+
if (!state.exists) {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
await runDocker(["rm", "-f", containerName]);
|
|
1800
|
+
}
|
|
1801
|
+
async function runDocker(args) {
|
|
1802
|
+
const { stdout } = await execFileAsync("docker", args, { encoding: "utf8" });
|
|
1803
|
+
return stdout.trim();
|
|
1804
|
+
}
|
|
1805
|
+
async function waitForStudio(port) {
|
|
1806
|
+
const spinner = new Spinner();
|
|
1807
|
+
spinner.start("Starting Studio");
|
|
1808
|
+
const startedAt = Date.now();
|
|
1809
|
+
try {
|
|
1810
|
+
while (Date.now() - startedAt < DEFAULT_HEALTH_TIMEOUT_MS) {
|
|
1811
|
+
try {
|
|
1812
|
+
const response = await fetch(`http://localhost:${port}${APP_HEALTH_PATH}`);
|
|
1813
|
+
if (response.ok) {
|
|
1814
|
+
const elapsed = Math.floor((Date.now() - startedAt) / 1e3);
|
|
1815
|
+
spinner.succeed(`Studio is ready${elapsed > 0 ? ` (${elapsed}s)` : ""}`);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
} catch {
|
|
1819
|
+
}
|
|
1820
|
+
await sleep2(1e3);
|
|
1821
|
+
}
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
spinner.fail("Studio failed to start");
|
|
1824
|
+
throw err;
|
|
1825
|
+
}
|
|
1826
|
+
spinner.fail("Studio timed out");
|
|
1827
|
+
throw new Error(`Studio did not become healthy within ${DEFAULT_HEALTH_TIMEOUT_MS / 1e3}s`);
|
|
1828
|
+
}
|
|
1829
|
+
async function buildStudioUrl(input3) {
|
|
1830
|
+
if (!input3.projectName) {
|
|
1831
|
+
return `http://localhost:${input3.port}`;
|
|
1832
|
+
}
|
|
1833
|
+
try {
|
|
1834
|
+
const response = await fetch(`http://localhost:${input3.port}/api/studio/license/status`, {
|
|
1835
|
+
headers: {
|
|
1836
|
+
"x-project-folder": input3.projectName
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
if (!response.ok) {
|
|
1840
|
+
return `http://localhost:${input3.port}/settings?tab=activation&project=${encodeURIComponent(input3.projectName)}`;
|
|
1841
|
+
}
|
|
1842
|
+
const status = await response.json();
|
|
1843
|
+
const targetPath = status.valid ? `/?project=${encodeURIComponent(input3.projectName)}` : `/settings?tab=activation&project=${encodeURIComponent(input3.projectName)}`;
|
|
1844
|
+
return `http://localhost:${input3.port}${targetPath}`;
|
|
1845
|
+
} catch {
|
|
1846
|
+
return `http://localhost:${input3.port}/?project=${encodeURIComponent(input3.projectName)}`;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
function openBrowser(url) {
|
|
1850
|
+
const child = process5.platform === "darwin" ? spawn4("open", [url], { detached: true, stdio: "ignore" }) : process5.platform === "win32" ? spawn4("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }) : spawn4("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
1851
|
+
child.unref();
|
|
1852
|
+
}
|
|
1853
|
+
async function findAvailablePort(startPort) {
|
|
1854
|
+
for (let port = startPort; port < startPort + 20; port += 1) {
|
|
1855
|
+
if (await isPortAvailable(port)) {
|
|
1856
|
+
return port;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
throw new Error(`Could not find an available port starting at ${startPort}`);
|
|
1860
|
+
}
|
|
1861
|
+
async function isPortAvailable(port) {
|
|
1862
|
+
return new Promise((resolve) => {
|
|
1863
|
+
const server = net.createServer();
|
|
1864
|
+
server.once("error", () => {
|
|
1865
|
+
resolve(false);
|
|
1866
|
+
});
|
|
1867
|
+
server.once("listening", () => {
|
|
1868
|
+
server.close(() => resolve(true));
|
|
1869
|
+
});
|
|
1870
|
+
server.listen(port, "127.0.0.1");
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
async function sleep2(ms) {
|
|
1874
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// src/commands/verify.ts
|
|
1878
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
1879
|
+
var verifyFlags = {
|
|
1880
|
+
token: { type: "string" },
|
|
1881
|
+
project: { type: "string", short: "p" }
|
|
1882
|
+
};
|
|
1883
|
+
async function runVerifyCommand(argv) {
|
|
1884
|
+
const parsed = parseArgs6({
|
|
1885
|
+
args: argv.slice(1),
|
|
1886
|
+
options: verifyFlags,
|
|
1887
|
+
allowPositionals: true
|
|
1888
|
+
});
|
|
1889
|
+
const config = await getConfig({
|
|
1890
|
+
token: parsed.values.token,
|
|
1891
|
+
projectName: parsed.values.project,
|
|
1892
|
+
yes: true
|
|
1893
|
+
});
|
|
1894
|
+
if (!config.token) {
|
|
1895
|
+
throw new Error("Missing SiteKnock token. Pass --token or set it in your env file.");
|
|
1896
|
+
}
|
|
1897
|
+
const result = await getEntitlement({
|
|
1898
|
+
licenseUrl: config.licenseUrl,
|
|
1899
|
+
token: config.token
|
|
1900
|
+
});
|
|
1901
|
+
console.log("License is valid");
|
|
1902
|
+
console.log(`Customer: ${result.customerName}`);
|
|
1903
|
+
console.log(`Plan: ${result.plan}`);
|
|
1904
|
+
console.log(`Version range: ${result.minVersion} -> ${result.maxVersion}`);
|
|
1905
|
+
console.log(`CLI seats (perpetual): ${result.cliSeats}`);
|
|
1906
|
+
console.log(`Studio seats: ${result.studioSeats}`);
|
|
1907
|
+
if (result.renewalActive) {
|
|
1908
|
+
console.log("Seat renewal: active \u2014 Studio access enabled.");
|
|
1909
|
+
} else {
|
|
1910
|
+
const expiry = result.expiresAt ? ` (lapsed ${result.expiresAt})` : "";
|
|
1911
|
+
console.log(`Seat renewal: inactive${expiry}.`);
|
|
1912
|
+
console.log(
|
|
1913
|
+
`Your perpetual license still lets you manage projects with siteknock on ${result.cliSeats} seat(s). Renew to restore Studio access and updates.`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// src/commands/kb.ts
|
|
1919
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1920
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
1921
|
+
import path11 from "path";
|
|
1922
|
+
import { parseArgs as parseArgs7 } from "util";
|
|
1923
|
+
import { pathToFileURL } from "url";
|
|
1924
|
+
|
|
1925
|
+
// src/lib/version-guard.ts
|
|
1926
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
1927
|
+
import path10 from "path";
|
|
1928
|
+
function readProjectSkVersion(projectDir) {
|
|
1929
|
+
const pkgPath = path10.join(projectDir, "package.json");
|
|
1930
|
+
if (!existsSync6(pkgPath)) return null;
|
|
1931
|
+
try {
|
|
1932
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1933
|
+
const v = pkg["sk-version"];
|
|
1934
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
1935
|
+
} catch {
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function readGeneratorVersion(generatorDir) {
|
|
1940
|
+
const pkgPath = path10.join(generatorDir, "package.json");
|
|
1941
|
+
if (!existsSync6(pkgPath)) return null;
|
|
1942
|
+
try {
|
|
1943
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1944
|
+
const v = pkg.version;
|
|
1945
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
1946
|
+
} catch {
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
function checkVersionMatch(projectDir, generatorDir) {
|
|
1951
|
+
const generatorVersion = readGeneratorVersion(generatorDir);
|
|
1952
|
+
const projectVersion = projectDir ? readProjectSkVersion(projectDir) : null;
|
|
1953
|
+
if (!generatorVersion) {
|
|
1954
|
+
return {
|
|
1955
|
+
match: false,
|
|
1956
|
+
projectVersion,
|
|
1957
|
+
generatorVersion: null,
|
|
1958
|
+
message: "Could not read generator version."
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
if (!projectVersion) {
|
|
1962
|
+
return {
|
|
1963
|
+
match: true,
|
|
1964
|
+
projectVersion: null,
|
|
1965
|
+
generatorVersion,
|
|
1966
|
+
message: `Using generator v${generatorVersion} (no project sk-version to compare).`
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
if (projectVersion === generatorVersion) {
|
|
1970
|
+
return {
|
|
1971
|
+
match: true,
|
|
1972
|
+
projectVersion,
|
|
1973
|
+
generatorVersion,
|
|
1974
|
+
message: `Generator v${generatorVersion} matches project sk-version.`
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
return {
|
|
1978
|
+
match: false,
|
|
1979
|
+
projectVersion,
|
|
1980
|
+
generatorVersion,
|
|
1981
|
+
message: `Version mismatch: project sk-version is ${projectVersion} but resolved generator is v${generatorVersion}. The KB/agent guides may not match your project's capabilities. Run \`pnpm install\` in the project to install the correct generator version, or use --project <folder> to target a specific project.`
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// src/commands/kb.ts
|
|
1986
|
+
var kbFlags = {
|
|
1987
|
+
dir: { type: "string" },
|
|
1988
|
+
project: { type: "string", short: "p" },
|
|
1989
|
+
force: { type: "boolean", default: false }
|
|
1990
|
+
};
|
|
1991
|
+
function kbLifecycleCandidates(workspaceRoot, projectDir) {
|
|
1992
|
+
const candidates = [];
|
|
1993
|
+
if (projectDir) {
|
|
1994
|
+
candidates.push(path11.join(workspaceRoot, projectDir, "node_modules", "@siteknock", "generator", "kb-lifecycle.mjs"));
|
|
1995
|
+
}
|
|
1996
|
+
candidates.push(path11.join(workspaceRoot, "node_modules", "@siteknock", "generator", "kb-lifecycle.mjs"));
|
|
1997
|
+
candidates.push(path11.join(workspaceRoot, "packages", "generator", "kb-lifecycle.mjs"));
|
|
1998
|
+
return candidates.filter((c) => existsSync7(c));
|
|
1999
|
+
}
|
|
2000
|
+
async function runKbCommand(argv) {
|
|
2001
|
+
const parsed = parseArgs7({
|
|
2002
|
+
args: argv.slice(1),
|
|
2003
|
+
options: kbFlags,
|
|
2004
|
+
allowPositionals: true
|
|
2005
|
+
});
|
|
2006
|
+
const subcommand = parsed.positionals[0];
|
|
2007
|
+
if (!subcommand || subcommand === "help" || parsed.values.force === void 0 && subcommand !== "dump") {
|
|
2008
|
+
if (subcommand !== "dump") {
|
|
2009
|
+
printKbHelp();
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (subcommand !== "dump") {
|
|
2014
|
+
printKbHelp();
|
|
2015
|
+
throw new Error(`Unknown kb subcommand: ${subcommand}`);
|
|
2016
|
+
}
|
|
2017
|
+
const workspaceRoot = await resolveWorkspaceRoot(process.cwd());
|
|
2018
|
+
const projectDir = parsed.values.project ? path11.join(workspaceRoot, parsed.values.project) : void 0;
|
|
2019
|
+
const candidates = kbLifecycleCandidates(workspaceRoot, projectDir);
|
|
2020
|
+
if (candidates.length === 0) {
|
|
2021
|
+
throw new Error(
|
|
2022
|
+
"Could not find @siteknock/generator (kb-lifecycle). Run `pnpm install` in a project first, or run from within the dev workspace."
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
const kbLifecyclePath = candidates[0];
|
|
2026
|
+
const generatorDir = path11.dirname(kbLifecyclePath);
|
|
2027
|
+
const versionCheck = checkVersionMatch(projectDir, generatorDir);
|
|
2028
|
+
if (!versionCheck.match) {
|
|
2029
|
+
console.warn(`\x1B[33m\u26A0 Version mismatch\x1B[0m: ${versionCheck.message}`);
|
|
2030
|
+
console.warn(`\x1B[33m\u26A0\x1B[0m The dumped KB may not match your project's capabilities.`);
|
|
2031
|
+
} else if (versionCheck.projectVersion) {
|
|
2032
|
+
console.log(
|
|
2033
|
+
`\x1B[32m\u2713\x1B[0m Generator v${versionCheck.generatorVersion} matches project sk-version ${versionCheck.projectVersion}`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
const mod = await import(pathToFileURL(kbLifecyclePath).href);
|
|
2037
|
+
const targetDir = parsed.values.dir ? path11.resolve(parsed.values.dir) : path11.join(workspaceRoot, ".agent", "siteknock-kb");
|
|
2038
|
+
await mkdir4(targetDir, { recursive: true });
|
|
2039
|
+
const result = await mod.dumpSiteknockKb(targetDir, { force: parsed.values.force });
|
|
2040
|
+
const manifest = await readWorkspaceManifest(workspaceRoot);
|
|
2041
|
+
const relativeTarget = path11.relative(workspaceRoot, targetDir) || ".";
|
|
2042
|
+
const updated = withSiteknockKb(manifest, { path: relativeTarget, version: result.version });
|
|
2043
|
+
await writeWorkspaceManifest(workspaceRoot, updated);
|
|
2044
|
+
console.log(`\x1B[32m\u2713\x1B[0m Dumped SiteKnock KB v${result.version} to ${targetDir}`);
|
|
2045
|
+
console.log(`\x1B[32m\u2713\x1B[0m ${result.written.length} file(s) written, ${result.skipped.length} preserved`);
|
|
2046
|
+
console.log(`\x1B[32m\u2713\x1B[0m Saved KB reference to ${path11.join(workspaceRoot, "sk-cli.json")}`);
|
|
2047
|
+
}
|
|
2048
|
+
function printKbHelp() {
|
|
2049
|
+
console.info(`
|
|
2050
|
+
\x1B[1m\x1B[36msiteknock kb\x1B[0m \u2014 SiteKnock Common KB management
|
|
2051
|
+
|
|
2052
|
+
\x1B[1mCommands:\x1B[0m
|
|
2053
|
+
dump [--dir <path>] [--project <folder>] [--force] Dump the Common KB
|
|
2054
|
+
|
|
2055
|
+
\x1B[1mOptions:\x1B[0m
|
|
2056
|
+
--dir <path> Target directory (default: <workspace>/.agent/siteknock-kb/)
|
|
2057
|
+
--project, -p <folder> Project to resolve generator from
|
|
2058
|
+
--force Overwrite existing files
|
|
2059
|
+
`);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/cli.ts
|
|
2063
|
+
var cliPackageInfo = getCliPackageInfo();
|
|
2064
|
+
async function main() {
|
|
2065
|
+
const args = process6.argv.slice(2);
|
|
2066
|
+
const envFile = getFlagValue(args, "--env-file");
|
|
2067
|
+
const commandArgs = stripFlagWithValue(args, "--env-file");
|
|
2068
|
+
const command = getCommand(commandArgs);
|
|
2069
|
+
if (!command && hasGlobalVersionFlag(commandArgs)) {
|
|
2070
|
+
printVersionBanner();
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
loadEnvFile(envFile);
|
|
2074
|
+
if (command === "create") {
|
|
2075
|
+
await runCreateCommand(commandArgs);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
if (command === "app") {
|
|
2079
|
+
await runAppCommand(commandArgs);
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
if (command === "locale") {
|
|
2083
|
+
await runLocaleCommand(commandArgs);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (command === "verify") {
|
|
2087
|
+
await runVerifyCommand(commandArgs);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (command === "studio") {
|
|
2091
|
+
await runStudioCommand(commandArgs);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (command === "probe") {
|
|
2095
|
+
await runProbeCommand(commandArgs);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (command === "kb") {
|
|
2099
|
+
await runKbCommand(commandArgs);
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
if (!command || command === "help" || commandArgs.includes("--help") || commandArgs.includes("-h")) {
|
|
2103
|
+
printHelp();
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
throw new Error(`Unknown command: ${command}`);
|
|
2107
|
+
}
|
|
2108
|
+
function getFlagValue(args, flag) {
|
|
2109
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2110
|
+
const arg = args[index];
|
|
2111
|
+
if (arg === flag) {
|
|
2112
|
+
return args[index + 1];
|
|
2113
|
+
}
|
|
2114
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
2115
|
+
return arg.slice(flag.length + 1);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
return void 0;
|
|
2119
|
+
}
|
|
2120
|
+
function stripFlagWithValue(args, flag) {
|
|
2121
|
+
const nextArgs = [];
|
|
2122
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2123
|
+
const arg = args[index];
|
|
2124
|
+
if (arg === flag) {
|
|
2125
|
+
index += 1;
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
nextArgs.push(arg);
|
|
2132
|
+
}
|
|
2133
|
+
return nextArgs;
|
|
2134
|
+
}
|
|
2135
|
+
function getCommand(args) {
|
|
2136
|
+
return args.find((value) => !value.startsWith("-"));
|
|
2137
|
+
}
|
|
2138
|
+
function hasGlobalVersionFlag(args) {
|
|
2139
|
+
return args.includes("--version") || args.includes("-v");
|
|
2140
|
+
}
|
|
2141
|
+
function printVersionBanner() {
|
|
2142
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2143
|
+
console.log(String.raw` ____ _ _ _ __ _
|
|
2144
|
+
/ ___|(_) |_ ___ | |/ /_ __ ___ ___| | __
|
|
2145
|
+
\___ \| | __/ _ \ | ' /| '_ \ / _ \ / __| |/ /
|
|
2146
|
+
___) | | || __/ | . \| | | | (_) | (__| <
|
|
2147
|
+
|____/|_|\__\___| |_|\_\_| |_|\___/ \___|_|\_\
|
|
2148
|
+
|
|
2149
|
+
SiteKnock CLI v${cliPackageInfo.version}
|
|
2150
|
+
Copyright (c) ${currentYear} Coversine LLC. All rights reserved.`);
|
|
2151
|
+
}
|
|
2152
|
+
function getCliPackageInfo() {
|
|
2153
|
+
const currentFilePath = fileURLToPath2(import.meta.url);
|
|
2154
|
+
let searchDirectory = path12.dirname(currentFilePath);
|
|
2155
|
+
while (true) {
|
|
2156
|
+
const packageJsonPath = path12.join(searchDirectory, "package.json");
|
|
2157
|
+
if (existsSync8(packageJsonPath)) {
|
|
2158
|
+
const packageJson = JSON.parse(readFileSync4(packageJsonPath, "utf8"));
|
|
2159
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
2160
|
+
return { version: packageJson.version };
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
const parentDirectory = path12.dirname(searchDirectory);
|
|
2164
|
+
if (parentDirectory === searchDirectory) {
|
|
2165
|
+
break;
|
|
2166
|
+
}
|
|
2167
|
+
searchDirectory = parentDirectory;
|
|
2168
|
+
}
|
|
2169
|
+
return { version: "unknown" };
|
|
2170
|
+
}
|
|
2171
|
+
function printHelp() {
|
|
2172
|
+
console.log(`Usage:
|
|
2173
|
+
siteknock --version
|
|
2174
|
+
siteknock create [options]
|
|
2175
|
+
siteknock app <create|list|remove> [options]
|
|
2176
|
+
siteknock locale <list|add> [options]
|
|
2177
|
+
siteknock probe [options]
|
|
2178
|
+
siteknock studio [start|stop|restart|status] [options]
|
|
2179
|
+
siteknock verify [options]
|
|
2180
|
+
siteknock kb dump [options]
|
|
2181
|
+
|
|
2182
|
+
sk-cli is available as a backwards-compatible alias for siteknock.
|
|
2183
|
+
|
|
2184
|
+
Commands:
|
|
2185
|
+
create Sign in through SiteKnock, download the licensed core bundle, and configure registry access
|
|
2186
|
+
app Create, list, or remove apps within a project (frontend / backend / fullstack / e2e)
|
|
2187
|
+
locale List kit languages or add a language to an app (seeds its locale files)
|
|
2188
|
+
probe Sign in through SiteKnock and verify registry visibility without creating files
|
|
2189
|
+
studio Run Studio against the current SiteKnock workspace (Docker container or Electron app)
|
|
2190
|
+
verify Verify token entitlement against the licensing server
|
|
2191
|
+
kb Dump the SiteKnock Common KB to a target directory
|
|
2192
|
+
|
|
2193
|
+
Common options:
|
|
2194
|
+
-h, --help Show help
|
|
2195
|
+
-v, --version Show the SiteKnock CLI version banner
|
|
2196
|
+
|
|
2197
|
+
Create options:
|
|
2198
|
+
--project, -p <folder> Top-level project folder inside the current SiteKnock workspace
|
|
2199
|
+
--version <semver> Requested SiteKnock version; validated against license range
|
|
2200
|
+
--yes Skip interactive confirmation prompts
|
|
2201
|
+
|
|
2202
|
+
Studio options:
|
|
2203
|
+
--mode, -m <mode> Launch mode: "docker" (default) or "app" (Electron desktop)
|
|
2204
|
+
--project, -p <folder> Project folder to open after Studio starts
|
|
2205
|
+
--port <port> Host port for Studio container (docker mode; saved in workspace)
|
|
2206
|
+
--container-name <id> Override the generated Studio container name (docker mode)
|
|
2207
|
+
--no-open Start Studio without opening the browser / app
|
|
2208
|
+
|
|
2209
|
+
Verify options:
|
|
2210
|
+
--token <token> SiteKnock license token
|
|
2211
|
+
--project, -p <folder> Read the saved token for a registered project from sk-cli.json
|
|
2212
|
+
|
|
2213
|
+
KB options:
|
|
2214
|
+
--dir <path> Target directory (default: <workspace>/.agent/siteknock-kb/)
|
|
2215
|
+
--project, -p <folder> Project to resolve generator from
|
|
2216
|
+
--force Overwrite existing files
|
|
2217
|
+
`);
|
|
2218
|
+
}
|
|
2219
|
+
main().catch((error) => {
|
|
2220
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2221
|
+
console.error(`ERROR: ${message}`);
|
|
2222
|
+
process6.exitCode = 1;
|
|
2223
|
+
});
|