@topolo/cli 0.2.1 → 0.2.2

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 (2) hide show
  1. package/dist/index.js +390 -50
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3312,16 +3312,6 @@ function writeContextCredential(config, environment, ctx, makeActive = true) {
3312
3312
  }
3313
3313
  return writeProfileConfig(config, environment, next);
3314
3314
  }
3315
- function activateContext(config, environment, key) {
3316
- const profile = profileForEnvironment(config, environment);
3317
- const ctx = profile.contexts?.[key];
3318
- if (!ctx) return config;
3319
- return writeProfileConfig(config, environment, {
3320
- activeContext: key,
3321
- credential: ctx.credential,
3322
- oauthClientId: ctx.oauthClientId ?? profile.oauthClientId
3323
- });
3324
- }
3325
3315
  function clearProfileCredential(config, environment) {
3326
3316
  const next = { ...config };
3327
3317
  if (environment === "production") {
@@ -5323,6 +5313,12 @@ function firstPartyAppPath(appDir, _audience) {
5323
5313
  return `apps/${appDir}`;
5324
5314
  }
5325
5315
 
5316
+ // src/commands/app-scaffold-cloudflare-constants.ts
5317
+ var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
5318
+ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
5319
+ var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
5320
+ var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
5321
+
5326
5322
  // src/commands/app-scaffold-package-template.ts
5327
5323
  function packageJson(application, target, options) {
5328
5324
  const dependencies = {};
@@ -5361,17 +5357,44 @@ function packageJson(application, target, options) {
5361
5357
  devDependencies["@cloudflare/workers-types"] = "^4.20260413.1";
5362
5358
  if (target === "platform") {
5363
5359
  dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
5360
+ dependencies["@topolo-io/auth-middleware"] = topoloPackageVersion("@topolo-io/auth-middleware", target);
5361
+ dependencies["@topolo-io/error-middleware"] = topoloPackageVersion("@topolo-io/error-middleware", target);
5362
+ dependencies["@topolo-io/security-headers"] = topoloPackageVersion("@topolo-io/security-headers", target);
5363
+ dependencies["@topolo-io/resource-context"] = topoloPackageVersion("@topolo-io/resource-context", target);
5364
+ dependencies["@topolo-io/data"] = topoloPackageVersion("@topolo-io/data", target);
5364
5365
  dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
5365
5366
  dependencies.hono = "^4";
5367
+ dependencies.zod = "^4.4.3";
5366
5368
  scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
5367
- scripts["deploy:staging"] = "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build && wrangler deploy --config apps/api/wrangler.toml --env staging && wrangler deploy --config apps/web/wrangler.toml --env staging";
5368
- scripts["deploy:production"] = "DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app npm run build && wrangler deploy --config apps/api/wrangler.toml --env production && wrangler deploy --config apps/web/wrangler.toml --env production";
5369
+ scripts["deploy:staging"] = `DEPLOY_ENVIRONMENT=staging CLOUDFLARE_ACCOUNT_ID=${STAGING_CLOUDFLARE_ACCOUNT_ID} python3 .github/scripts/standard_deploy.py local-deploy`;
5370
+ scripts["deploy:production"] = `DEPLOY_ENVIRONMENT=production CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID} python3 .github/scripts/standard_deploy.py local-deploy`;
5369
5371
  } else {
5370
5372
  dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
5371
5373
  scripts.deploy = "npm run build && wrangler deploy";
5372
5374
  }
5373
5375
  devDependencies.wrangler = "^4.91.0";
5374
5376
  }
5377
+ if (target === "platform") {
5378
+ const testDirs = [];
5379
+ if (options.browser) testDirs.push("apps/web");
5380
+ if (options.api) testDirs.push("apps/api");
5381
+ if (testDirs.length > 0) {
5382
+ const chain = (withCoverage) => testDirs.map((dir, index) => {
5383
+ const enter = index === 0 ? `cd ${dir}` : `cd ../${dir.split("/").pop()}`;
5384
+ return `${enter} && vitest run --config vitest.config.ts --passWithNoTests${withCoverage ? " --coverage" : ""}`;
5385
+ }).join(" && ");
5386
+ scripts.test = chain(false);
5387
+ scripts.coverage = chain(true);
5388
+ scripts.typecheck = testDirs.map((dir) => `tsc --noEmit -p ${dir}/tsconfig.json`).join(" && ");
5389
+ devDependencies.vitest = "^3.2.4";
5390
+ devDependencies["@vitest/coverage-v8"] = "^3.2.4";
5391
+ if (options.browser) {
5392
+ devDependencies.jsdom = "^29.1.1";
5393
+ devDependencies["@testing-library/react"] = "^16.3.2";
5394
+ devDependencies["@testing-library/user-event"] = "^14.6.1";
5395
+ }
5396
+ }
5397
+ }
5375
5398
  if (target === "platform" && hasScaffoldResources(options.resources)) {
5376
5399
  scripts["check:deploy-config"] = "wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build && wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build";
5377
5400
  }
@@ -5400,7 +5423,11 @@ function topoloPackageVersion(packageName, target) {
5400
5423
  "@topolo-io/app-launcher": "^0.1.15",
5401
5424
  "@topolo-io/app-shell": "^0.5.54",
5402
5425
  "@topolo-io/auth-client": "^1.2.1",
5403
- "@topolo-io/auth-middleware": "^0.3.4",
5426
+ "@topolo-io/auth-middleware": "^0.4.0",
5427
+ "@topolo-io/error-middleware": "^0.6.0",
5428
+ "@topolo-io/security-headers": "^0.2.2",
5429
+ "@topolo-io/resource-context": "^0.3.0",
5430
+ "@topolo-io/data": "^0.1.2",
5404
5431
  "@topolo-io/worker-runtime": "^0.1.11",
5405
5432
  "@topolo-io/design-tokens": "^0.1.8",
5406
5433
  "@topolo-io/onboarding": "^0.9.5",
@@ -5436,7 +5463,12 @@ function resourceConfig(application, profile) {
5436
5463
  workflowProductionName: `topolo-${slug}-import`,
5437
5464
  workflowStagingName: `topolo-${slug}-import-staging`,
5438
5465
  containerProductionName: `topolo-${slug}-processor`,
5439
- containerStagingName: `topolo-${slug}-processor-staging`
5466
+ containerStagingName: `topolo-${slug}-processor-staging`,
5467
+ d1DevelopmentName: `topolo-${slug}-core-development`,
5468
+ r2DevelopmentName: `topolo-${slug}-assets-development`,
5469
+ queueDevelopmentName: `topolo-${slug}-processing-development`,
5470
+ workflowDevelopmentName: `topolo-${slug}-import-development`,
5471
+ containerDevelopmentName: `topolo-${slug}-processor-development`
5440
5472
  };
5441
5473
  }
5442
5474
  return {
@@ -5454,7 +5486,12 @@ function resourceConfig(application, profile) {
5454
5486
  workflowProductionName: `topolo-${slug}-workflow`,
5455
5487
  workflowStagingName: `topolo-${slug}-workflow-staging`,
5456
5488
  containerProductionName: `topolo-${slug}-processor`,
5457
- containerStagingName: `topolo-${slug}-processor-staging`
5489
+ containerStagingName: `topolo-${slug}-processor-staging`,
5490
+ d1DevelopmentName: `topolo-${slug}-core-development`,
5491
+ r2DevelopmentName: `topolo-${slug}-assets-development`,
5492
+ queueDevelopmentName: `topolo-${slug}-queue-development`,
5493
+ workflowDevelopmentName: `topolo-${slug}-workflow-development`,
5494
+ containerDevelopmentName: `topolo-${slug}-processor-development`
5458
5495
  };
5459
5496
  }
5460
5497
 
@@ -5627,6 +5664,64 @@ function tsconfigJson(api, target) {
5627
5664
  })}
5628
5665
  `;
5629
5666
  }
5667
+ var TSCONFIG_TEST_EXCLUDE = [
5668
+ "src/**/*.test.ts",
5669
+ "src/**/*.test.tsx",
5670
+ "src/**/*.test.mts",
5671
+ "src/**/*.spec.ts",
5672
+ "src/**/*.spec.tsx",
5673
+ "test"
5674
+ ];
5675
+ function webTsconfigJson() {
5676
+ return `${json({
5677
+ compilerOptions: {
5678
+ target: "ES2022",
5679
+ useDefineForClassFields: true,
5680
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
5681
+ allowJs: false,
5682
+ skipLibCheck: true,
5683
+ esModuleInterop: true,
5684
+ allowSyntheticDefaultImports: true,
5685
+ strict: true,
5686
+ forceConsistentCasingInFileNames: true,
5687
+ module: "ESNext",
5688
+ moduleResolution: "Bundler",
5689
+ resolveJsonModule: true,
5690
+ isolatedModules: true,
5691
+ noEmit: true,
5692
+ jsx: "react-jsx",
5693
+ types: ["vite/client"]
5694
+ },
5695
+ include: ["src"],
5696
+ references: [],
5697
+ exclude: TSCONFIG_TEST_EXCLUDE
5698
+ })}
5699
+ `;
5700
+ }
5701
+ function apiTsconfigJson() {
5702
+ return `${json({
5703
+ compilerOptions: {
5704
+ target: "ES2022",
5705
+ lib: ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
5706
+ allowJs: false,
5707
+ skipLibCheck: true,
5708
+ esModuleInterop: true,
5709
+ allowSyntheticDefaultImports: true,
5710
+ strict: true,
5711
+ forceConsistentCasingInFileNames: true,
5712
+ module: "ESNext",
5713
+ moduleResolution: "Bundler",
5714
+ resolveJsonModule: true,
5715
+ isolatedModules: true,
5716
+ noEmit: true,
5717
+ types: ["@cloudflare/workers-types", "node"]
5718
+ },
5719
+ include: ["src"],
5720
+ references: [],
5721
+ exclude: TSCONFIG_TEST_EXCLUDE
5722
+ })}
5723
+ `;
5724
+ }
5630
5725
  function gitignore() {
5631
5726
  return [
5632
5727
  "node_modules/",
@@ -6368,11 +6463,6 @@ function developerAppTsx(application) {
6368
6463
  ].join("\n");
6369
6464
  }
6370
6465
 
6371
- // src/commands/app-scaffold-cloudflare-constants.ts
6372
- var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
6373
- var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
6374
- var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
6375
-
6376
6466
  // src/commands/app-scaffold-worker-template.ts
6377
6467
  function webWorkerTs(application) {
6378
6468
  const serviceName = toTitle(application.name);
@@ -6427,6 +6517,29 @@ function platformWebWranglerToml(application) {
6427
6517
  'binding = "API"',
6428
6518
  `service = "topolo-${slug}-api"`,
6429
6519
  "",
6520
+ "[env.development]",
6521
+ `name = "topolo-${slug}-development"`,
6522
+ `account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
6523
+ "workers_dev = true",
6524
+ "",
6525
+ "[env.development.vars]",
6526
+ `APP_ID = "${application.appId ?? ""}"`,
6527
+ `VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
6528
+ "",
6529
+ "[[env.development.routes]]",
6530
+ `pattern = "${slug}.topolo.dev"`,
6531
+ "custom_domain = true",
6532
+ "",
6533
+ "[env.development.assets]",
6534
+ 'directory = "./dist"',
6535
+ 'binding = "ASSETS"',
6536
+ 'not_found_handling = "single-page-application"',
6537
+ 'run_worker_first = ["/api/*"]',
6538
+ "",
6539
+ "[[env.development.services]]",
6540
+ 'binding = "API"',
6541
+ `service = "topolo-${slug}-api-development"`,
6542
+ "",
6430
6543
  "[env.staging]",
6431
6544
  `name = "topolo-${slug}-staging"`,
6432
6545
  `account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
@@ -6498,6 +6611,22 @@ function platformApiWranglerToml(application, resources, profile) {
6498
6611
  ""
6499
6612
  ];
6500
6613
  appendResourceWranglerBlocks(lines, resources, resource, "production");
6614
+ lines.push(
6615
+ "[env.development]",
6616
+ `name = "topolo-${slug}-api-development"`,
6617
+ `account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
6618
+ "workers_dev = true",
6619
+ "",
6620
+ "[env.development.vars]",
6621
+ 'AUTH_BASE_URL = "https://auth.topolo.dev"',
6622
+ 'NEXUS_GATEWAY_URL = "https://nexus.topolo.dev"',
6623
+ `NEXUS_APP_ID = "${slug}"`,
6624
+ `APP_ID = "${application.appId ?? ""}"`,
6625
+ `VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
6626
+ 'ENVIRONMENT = "development"',
6627
+ ""
6628
+ );
6629
+ appendResourceWranglerBlocks(lines, resources, resource, "development");
6501
6630
  lines.push(
6502
6631
  "[env.staging]",
6503
6632
  `name = "topolo-${slug}-api-staging"`,
@@ -6531,13 +6660,14 @@ function platformApiWranglerToml(application, resources, profile) {
6531
6660
  return lines.join("\n");
6532
6661
  }
6533
6662
  function appendResourceWranglerBlocks(lines, resources, resource, environment) {
6534
- const prefix = environment === "staging" ? "env.staging." : environment === "production-env" ? "env.production." : "";
6535
- const envName = environment === "staging" ? "Staging" : "Production";
6663
+ const prefix = environment === "staging" ? "env.staging." : environment === "development" ? "env.development." : environment === "production-env" ? "env.production." : "";
6664
+ const envName = environment === "staging" ? "Staging" : environment === "development" ? "Development" : "Production";
6665
+ const pick = (dev, staging, prod) => environment === "development" ? dev : environment === "staging" ? staging : prod;
6536
6666
  if (resources.d1) {
6537
6667
  lines.push(
6538
6668
  `[[${prefix}d1_databases]]`,
6539
6669
  `binding = "${resource.d1Binding}"`,
6540
- `database_name = "${environment === "staging" ? resource.d1StagingName : resource.d1ProductionName}"`,
6670
+ `database_name = "${pick(resource.d1DevelopmentName, resource.d1StagingName, resource.d1ProductionName)}"`,
6541
6671
  'database_id = "provision-after-scaffold"',
6542
6672
  'migrations_dir = "migrations"',
6543
6673
  ""
@@ -6547,7 +6677,7 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
6547
6677
  lines.push(
6548
6678
  `[[${prefix}r2_buckets]]`,
6549
6679
  `binding = "${resource.r2Binding}"`,
6550
- `bucket_name = "${environment === "staging" ? resource.r2StagingName : resource.r2ProductionName}"`,
6680
+ `bucket_name = "${pick(resource.r2DevelopmentName, resource.r2StagingName, resource.r2ProductionName)}"`,
6551
6681
  ""
6552
6682
  );
6553
6683
  }
@@ -6555,10 +6685,10 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
6555
6685
  lines.push(
6556
6686
  `[[${prefix}queues.producers]]`,
6557
6687
  `binding = "${resource.queueBinding}"`,
6558
- `queue = "${environment === "staging" ? resource.queueStagingName : resource.queueProductionName}"`,
6688
+ `queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
6559
6689
  "",
6560
6690
  `[[${prefix}queues.consumers]]`,
6561
- `queue = "${environment === "staging" ? resource.queueStagingName : resource.queueProductionName}"`,
6691
+ `queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
6562
6692
  ""
6563
6693
  );
6564
6694
  }
@@ -6566,7 +6696,7 @@ function appendResourceWranglerBlocks(lines, resources, resource, environment) {
6566
6696
  lines.push(
6567
6697
  `[[${prefix}workflows]]`,
6568
6698
  `binding = "${resource.workflowBinding}"`,
6569
- `name = "${environment === "staging" ? resource.workflowStagingName : resource.workflowProductionName}"`,
6699
+ `name = "${pick(resource.workflowDevelopmentName, resource.workflowStagingName, resource.workflowProductionName)}"`,
6570
6700
  `class_name = "${resource.workflowClassName}"`,
6571
6701
  ""
6572
6702
  );
@@ -6764,6 +6894,8 @@ function platformWorkerTs(application, options) {
6764
6894
  "import { Hono } from 'hono';",
6765
6895
  "import { createTopoloWidgetResponse, type TopoloWidgetDraft } from '@topolo/sdk';",
6766
6896
  "import { createTopoloWorkerRuntime, type TopoloRuntimeContext } from '@topolo-io/worker-runtime';",
6897
+ "import { withSecurityHeaders } from '@topolo-io/security-headers';",
6898
+ "import { withErrorReporting } from '@topolo-io/error-middleware';",
6767
6899
  "import { SCAFFOLD_APP_ID, SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeAppId } from './app-identity';",
6768
6900
  ...options.resources.workflow ? ["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';"] : [],
6769
6901
  "",
@@ -6854,11 +6986,20 @@ function platformWorkerTs(application, options) {
6854
6986
  " return handleRequest(c.req.raw, c.env, rt);",
6855
6987
  "});",
6856
6988
  "",
6989
+ "// Platform security headers + structured error reporting wrap the whole app",
6990
+ "// (outermost), so headers are stamped even on error-middleware 5xx responses.",
6991
+ "const securedFetch = withSecurityHeaders(",
6992
+ " withErrorReporting(",
6993
+ " (request: Request, env: Env, ctx: ExecutionContext) => api.fetch(request, env, ctx),",
6994
+ " { appName: SCAFFOLD_APP_NAME, appId: SCAFFOLD_SERVICE_SLUG },",
6995
+ " ),",
6996
+ ") as (request: Request, env: Env, ctx: ExecutionContext) => Response | Promise<Response>;",
6997
+ "",
6857
6998
  "export default {",
6858
6999
  " fetch: runtime.worker((request, env, ctx, rt) => {",
6859
7000
  " const bindings = env as Env & { __rt?: TopoloRuntimeContext<Env> };",
6860
7001
  " bindings.__rt = rt;",
6861
- " return api.fetch(request, bindings, ctx as ExecutionContext);",
7002
+ " return securedFetch(request, bindings, ctx as ExecutionContext);",
6862
7003
  " }),",
6863
7004
  "};",
6864
7005
  "",
@@ -7211,9 +7352,12 @@ function topoloAppManifestJson(application, options) {
7211
7352
  $schema: "./../../topolo-platform/tooling/app-manifest-schema.json",
7212
7353
  name: `topolo-${slug}`,
7213
7354
  appId: application.appId,
7214
- primaryTarget: "web",
7215
7355
  compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
7216
7356
  environments: {
7357
+ development: {
7358
+ accountId: DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID,
7359
+ domain: "topolo.dev"
7360
+ },
7217
7361
  staging: {
7218
7362
  accountId: STAGING_CLOUDFLARE_ACCOUNT_ID,
7219
7363
  domain: "stg.topolo.us"
@@ -7254,6 +7398,15 @@ function cloudcontrolJson(application, options) {
7254
7398
  resource_name: resources.d1StagingName,
7255
7399
  resource_id: "provision-after-scaffold",
7256
7400
  migrations_dir: "migrations"
7401
+ },
7402
+ {
7403
+ resource_type: "d1",
7404
+ binding: resources.d1Binding,
7405
+ environment: "development",
7406
+ account_id: DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID,
7407
+ resource_name: resources.d1DevelopmentName,
7408
+ resource_id: "provision-after-scaffold",
7409
+ migrations_dir: "migrations"
7257
7410
  }
7258
7411
  );
7259
7412
  }
@@ -7874,6 +8027,74 @@ function envExample(application) {
7874
8027
  function generateOpaqueAppId() {
7875
8028
  return `app_${randomBytes(9).toString("base64url")}`;
7876
8029
  }
8030
+ function standardScriptStub() {
8031
+ return `#!/usr/bin/env python3
8032
+ """Canonical-fetching stub for .github/scripts/standard_*.py.
8033
+
8034
+ Replaces a vendored copy. At runtime, finds the canonical sibling in
8035
+ \`<repo-parent>/topolo-platform/tooling/<name>.py\` and execs it, passing argv
8036
+ through, so every consumer runs the current platform script on every
8037
+ invocation (no drifted per-app copies).
8038
+
8039
+ Override: set TOPOLO_PLATFORM_DIR to the absolute path of the topolo-platform
8040
+ repo when the canonical is in a non-default checkout layout (e.g. CI). If no
8041
+ canonical can be found, exits 2 with a clear message rather than silently
8042
+ falling back.
8043
+ """
8044
+ from __future__ import annotations
8045
+
8046
+ import os
8047
+ import sys
8048
+
8049
+ STUB_NAME = os.path.basename(__file__) # standard_ci.py OR standard_deploy.py
8050
+ HERE = os.path.dirname(os.path.abspath(__file__))
8051
+
8052
+
8053
+ def find_canonical() -> str | None:
8054
+ # 1. Explicit override (CI runners with non-standard layouts).
8055
+ override = os.environ.get('TOPOLO_PLATFORM_DIR')
8056
+ if override:
8057
+ path = os.path.join(override, 'tooling', STUB_NAME)
8058
+ if os.path.exists(path):
8059
+ return path
8060
+ # 2. Walk up looking for a sibling topolo-platform/tooling/<name>.py.
8061
+ cursor = HERE
8062
+ for _ in range(10):
8063
+ candidate = os.path.normpath(os.path.join(cursor, '..', 'topolo-platform', 'tooling', STUB_NAME))
8064
+ if os.path.exists(candidate):
8065
+ return candidate
8066
+ parent = os.path.dirname(cursor)
8067
+ if parent == cursor:
8068
+ break
8069
+ cursor = parent
8070
+ # 3. node_modules-based delivery (future): @topolo-io/standard-ci.
8071
+ cursor = HERE
8072
+ for _ in range(10):
8073
+ candidate = os.path.normpath(os.path.join(cursor, 'node_modules', '@topolo-io', 'standard-ci', 'bin', STUB_NAME))
8074
+ if os.path.exists(candidate):
8075
+ return candidate
8076
+ parent = os.path.dirname(cursor)
8077
+ if parent == cursor:
8078
+ break
8079
+ cursor = parent
8080
+ return None
8081
+
8082
+
8083
+ def main() -> None:
8084
+ canonical = find_canonical()
8085
+ if not canonical:
8086
+ sys.stderr.write(
8087
+ f'[topolo-canonical-stub] could not locate canonical {STUB_NAME}.\\n'
8088
+ f' Set TOPOLO_PLATFORM_DIR or checkout topolo-platform as a sibling of this app.\\n'
8089
+ )
8090
+ sys.exit(2)
8091
+ os.execv(sys.executable, [sys.executable, canonical, *sys.argv[1:]])
8092
+
8093
+
8094
+ if __name__ == '__main__':
8095
+ main()
8096
+ `;
8097
+ }
7877
8098
  function applicationScaffoldPayload(application, options = {}) {
7878
8099
  const target = normalizeTarget(options.target);
7879
8100
  const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
@@ -7892,6 +8113,9 @@ function applicationScaffoldPayload(application, options = {}) {
7892
8113
  const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
7893
8114
  const profile = normalizeScaffoldProfile(options.profile);
7894
8115
  const resources = resolveScaffoldResources(profile, options);
8116
+ if (target === "platform" && api && !profile) {
8117
+ resources.d1 = true;
8118
+ }
7895
8119
  const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
7896
8120
  const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
7897
8121
  const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
@@ -8038,6 +8262,28 @@ function scaffoldCapabilities(options) {
8038
8262
  function scaffoldFiles(options) {
8039
8263
  const { application, target } = options;
8040
8264
  const slug = toSlug(application.id);
8265
+ const tablePrefix = slug.replace(/-/g, "_");
8266
+ const tenantColumn = options.payload.tenancy === "household" ? "household_id" : "organization_id";
8267
+ const workspacesMigrationSql = [
8268
+ `CREATE TABLE IF NOT EXISTS ${tablePrefix}_workspaces (`,
8269
+ " id TEXT PRIMARY KEY,",
8270
+ ` ${tenantColumn} TEXT NOT NULL,`,
8271
+ " name TEXT NOT NULL,",
8272
+ " slug TEXT NOT NULL,",
8273
+ " created_by_user_id TEXT,",
8274
+ " created_by_email TEXT,",
8275
+ " created_at TEXT NOT NULL,",
8276
+ " updated_at TEXT NOT NULL,",
8277
+ ` UNIQUE(${tenantColumn}, slug)`,
8278
+ ");",
8279
+ "",
8280
+ `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workspaces_${tenantColumn}`,
8281
+ ` ON ${tablePrefix}_workspaces(${tenantColumn});`,
8282
+ "",
8283
+ `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workspaces_slug`,
8284
+ ` ON ${tablePrefix}_workspaces(${tenantColumn}, slug);`,
8285
+ ""
8286
+ ].join("\n");
8041
8287
  const base = target === "platform" ? firstPartyAppPath(application.appDir, options.payload.audience) : application.appDir;
8042
8288
  const browserBase = target === "platform" ? `${base}/apps/web` : base;
8043
8289
  const apiBase = target === "platform" ? `${base}/apps/api` : base;
@@ -8095,15 +8341,6 @@ function scaffoldFiles(options) {
8095
8341
  resources: options.payload.resources
8096
8342
  })
8097
8343
  },
8098
- {
8099
- path: `${base}/tsconfig.json`,
8100
- purpose: "TypeScript configuration for the generated app.",
8101
- requiredSignals: [
8102
- "uses strict mode",
8103
- "includes browser DOM types for React and Worker types for API-backed apps"
8104
- ],
8105
- content: tsconfigJson(options.api, target)
8106
- },
8107
8344
  {
8108
8345
  path: `${browserBase}/vite.config.ts`,
8109
8346
  purpose: "Vite browser build configuration.",
@@ -8161,6 +8398,31 @@ function scaffoldFiles(options) {
8161
8398
  content: stylesCss()
8162
8399
  }
8163
8400
  ];
8401
+ if (target === "platform") {
8402
+ if (options.browser) {
8403
+ files.push({
8404
+ path: `${browserBase}/tsconfig.json`,
8405
+ purpose: "TypeScript configuration for the browser surface (DOM + vite types).",
8406
+ requiredSignals: ["uses strict mode", "includes browser DOM types for React"],
8407
+ content: webTsconfigJson()
8408
+ });
8409
+ }
8410
+ if (options.api) {
8411
+ files.push({
8412
+ path: `${apiBase}/tsconfig.json`,
8413
+ purpose: "TypeScript configuration for the API worker surface (Workers types).",
8414
+ requiredSignals: ["uses strict mode", "includes Cloudflare Workers types"],
8415
+ content: apiTsconfigJson()
8416
+ });
8417
+ }
8418
+ } else {
8419
+ files.push({
8420
+ path: `${base}/tsconfig.json`,
8421
+ purpose: "TypeScript configuration for the generated app.",
8422
+ requiredSignals: ["uses strict mode", "includes browser DOM types for React and Worker types for API-backed apps"],
8423
+ content: tsconfigJson(options.api, target)
8424
+ });
8425
+ }
8164
8426
  if (target === "platform") {
8165
8427
  files.push(
8166
8428
  {
@@ -8201,6 +8463,33 @@ function scaffoldFiles(options) {
8201
8463
  ],
8202
8464
  content: options.ciWorkflow ?? ""
8203
8465
  },
8466
+ {
8467
+ path: `${base}/.github/scripts/standard_deploy.py`,
8468
+ purpose: "Canonical-fetching deploy stub (execs topolo-platform/tooling/standard_deploy.py).",
8469
+ requiredSignals: [
8470
+ "never a vendored copy of the canonical script",
8471
+ "runs the current platform standard_deploy.py on every invocation"
8472
+ ],
8473
+ content: standardScriptStub()
8474
+ },
8475
+ {
8476
+ path: `${base}/.github/scripts/standard_ci.py`,
8477
+ purpose: "Canonical-fetching CI stub (execs topolo-platform/tooling/standard_ci.py).",
8478
+ requiredSignals: [
8479
+ "never a vendored copy of the canonical script",
8480
+ "runs the current platform standard_ci.py on every invocation"
8481
+ ],
8482
+ content: standardScriptStub()
8483
+ },
8484
+ {
8485
+ path: `${base}/.github/scripts/standard_ops.py`,
8486
+ purpose: "Canonical-fetching ops stub (execs topolo-platform/tooling/standard_ops.py).",
8487
+ requiredSignals: [
8488
+ "never a vendored copy of the canonical script",
8489
+ "runs the current platform standard_ops.py on every invocation"
8490
+ ],
8491
+ content: standardScriptStub()
8492
+ },
8204
8493
  {
8205
8494
  path: `system-apps/TopoloDocs/apps/web/src/content/systems/topolo-${slug}.json`,
8206
8495
  purpose: "Canonical system registry entry.",
@@ -8324,6 +8613,25 @@ function scaffoldFiles(options) {
8324
8613
  portfolio: options.payload.portfolio
8325
8614
  })
8326
8615
  });
8616
+ if (target === "platform") {
8617
+ files.push({
8618
+ path: `${browserBase}/vitest.config.ts`,
8619
+ purpose: "Vitest config for the browser surface.",
8620
+ requiredSignals: [
8621
+ "runs component/unit tests under jsdom",
8622
+ "keeps the generated browser surface testable from day one"
8623
+ ],
8624
+ content: `import { defineConfig } from 'vitest/config';
8625
+
8626
+ export default defineConfig({
8627
+ test: {
8628
+ include: ['test/**/*.test.{ts,tsx}'],
8629
+ environment: 'jsdom',
8630
+ },
8631
+ });
8632
+ `
8633
+ });
8634
+ }
8327
8635
  }
8328
8636
  if (options.api) {
8329
8637
  files.push(
@@ -8380,8 +8688,45 @@ function scaffoldFiles(options) {
8380
8688
  "binds the frontend Worker to the API Worker for /api/* requests"
8381
8689
  ],
8382
8690
  content: platformWebWranglerToml(application)
8691
+ },
8692
+ {
8693
+ path: `${apiBase}/vitest.config.ts`,
8694
+ purpose: "Vitest config for the API worker surface.",
8695
+ requiredSignals: [
8696
+ "runs worker/unit tests under the node environment",
8697
+ "keeps the generated service testable from day one"
8698
+ ],
8699
+ content: `import { defineConfig } from 'vitest/config';
8700
+
8701
+ export default defineConfig({
8702
+ test: {
8703
+ include: ['test/**/*.test.{js,mjs,mts,ts,tsx}'],
8704
+ environment: 'node',
8705
+ },
8706
+ });
8707
+ `
8708
+ },
8709
+ {
8710
+ path: `${apiBase}/test/health.test.ts`,
8711
+ purpose: "Smoke test so the API test suite is non-empty from scaffold.",
8712
+ requiredSignals: [
8713
+ "asserts the scaffold boots",
8714
+ "is replaced by real route/repository tests as the surface grows"
8715
+ ],
8716
+ content: "import { describe, expect, it } from 'vitest';\n\n// Smoke test so the API test suite is non-empty from day one. Replace with\n// real route/repository tests as the surface grows.\ndescribe('api scaffold', () => {\n it('boots', () => {\n expect(true).toBe(true);\n });\n});\n"
8383
8717
  }
8384
8718
  );
8719
+ if (options.payload.resources.d1) {
8720
+ files.push({
8721
+ path: `${apiBase}/migrations/0001_initial.sql`,
8722
+ purpose: "Canonical multi-tenant baseline: the app workspaces table.",
8723
+ requiredSignals: [
8724
+ `${tablePrefix}_workspaces carries ${tenantColumn} on every row`,
8725
+ "every domain table added later carries the tenant + workspace columns"
8726
+ ],
8727
+ content: workspacesMigrationSql
8728
+ });
8729
+ }
8385
8730
  }
8386
8731
  }
8387
8732
  return files;
@@ -10299,18 +10644,13 @@ function registerContextCommand(program2) {
10299
10644
  }
10300
10645
  const key = contextKey(resolved.contextType, resolved.organizationId);
10301
10646
  const existing = profile.contexts?.[key];
10302
- let updated = config;
10303
- if (existing?.credential) {
10304
- updated = activateContext(config, environment, key);
10305
- } else {
10306
- const newCredential = await exchangeForContext(cred, serviceUrls, {
10307
- type: resolved.contextType,
10308
- id: resolved.organizationId
10309
- });
10310
- resolved.credential = newCredential;
10311
- resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
10312
- updated = writeContextCredential(config, environment, resolved, true);
10313
- }
10647
+ const newCredential = await exchangeForContext(cred, serviceUrls, {
10648
+ type: resolved.contextType,
10649
+ id: resolved.organizationId
10650
+ });
10651
+ resolved.credential = newCredential;
10652
+ resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
10653
+ const updated = writeContextCredential(config, environment, resolved, true);
10314
10654
  await writeConfig(updated);
10315
10655
  emit(
10316
10656
  { environment, active: key, context: { type: resolved.contextType, label: labelFor(resolved) } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
5
5
  "type": "module",
6
6
  "bin": {