@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.
Files changed (4) hide show
  1. package/README.md +70 -0
  2. package/cli.d.ts +1 -0
  3. package/cli.js +2223 -0
  4. 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
+ });